docs: M2 开工前分析 10 份报告入档 + ADR-009~015 拍板决策
CI / docs-build (push) Successful in 1m3s

- iteration-2 报告 01-08(PM 拆解/后端/Flutter 评估/现实核查/UI 规范/埋点规划/证据基线/Git 规划),04、06 已由正式角色复核定稿
- mkdocs 挂「第二迭代」导航,build --strict 通过
- ADR-009 新建 patbond-pet 模块、ADR-010 照片剪出 M2、ADR-011 dev 主干/master 发布、ADR-012 北极星与 H1-H4、ADR-013 废弃 health_record_action、ADR-014 DEBT-1 随 M2、ADR-015 照护人邀请后置

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 13:54:35 +08:00
parent 5537f92227
commit 1891d9b7b4
10 changed files with 2007 additions and 0 deletions
@@ -0,0 +1,346 @@
# Patbond 第二迭代任务分解(M2 宠物健康档案)
> 作者:Senior Project Manager
> 日期:2026-09-07
> 依据:`patbond-doc/docs/development/development-plan.md`(第 7 节 M2、第 6.2 节 Pets 接口、第 9/10 节质量门禁与 DoD)、`iterations/iteration-1/20-iteration-1-summary.md`(收官总结与遗留清单)、`docs/architecture/decisions.md`ADR-001~008)、`docs/database/patbond_postgresql.sql``pet_health` schema 8 张表)
> 编号约定:本迭代工单以 `T2-` 前缀编号(T2-01 起),避免与第一迭代 T0-x/T1~T12 冲突。
> 范围声明:严格限定为 M2 宠物健康档案。社区(M3)、AI 创作(M4)、预约(M5)不在本迭代范围;发现范围外需求一律记入 backlog。
---
## 1. 范围界定与依据
### 1.1 开发计划 M2 原文(正典依据)
开发计划第 7 节 M2 定义(引用原文要点):
- 目标:"替换 Flutter 档案页的本地宠物和疫苗数据。"
- "实现宠物、照护权限、体重、疫苗、健康事件和提醒接口。"
- "校验当前用户对宠物的 owner/caregiver/viewer 权限。"
- "Flutter 接入真实列表、详情、编辑、加载、空态和错误态。"
- "体重、疫苗进度、下次接种和月度花费从事实表聚合生成。"
- 验收标准:"数据可跨设备读取;无权限用户不能访问宠物;并发更新返回明确冲突;关键流程具备 API 集成测试和 Flutter 组件测试。"
第 6.2 节第一批接口中属于本迭代的端点:
| 端点 | 用途 |
| --- | --- |
| `GET/POST /api/v1/pets` | 查询、新建宠物 |
| `GET/PATCH /api/v1/pets/{petId}` | 宠物详情与更新 |
| `GET/POST /api/v1/pets/{petId}/weights` | 体重记录 |
| `GET/POST/PATCH /api/v1/pets/{petId}/vaccinations` | 疫苗记录 |
| `GET/POST /api/v1/pets/{petId}/health-events` | 健康时间线 |
数据模型依据:`pet_health` schema 共 8 张表(breeds、pets、pet_owners、pet_weight_records、vaccine_catalog、pet_vaccinations、health_events、health_event_media、care_reminders——含关联表 9 个对象),字段、约束、状态机均已在 bootstrap SQL 定稿评审。
### 1.2 与第一迭代总结口径的差异(须拍板,见 §4)
第一迭代总结把 M2 目标写为"档案 CRUD、照片管理、体重/**体温**记录、疫苗/驱虫提醒",与开发计划 M2 原文存在三处扩张,PM 逐条对照数据模型后的结论:
1. **照片管理**`pets.avatar_asset_id``pet_vaccinations.certificate_asset_id``health_event_media` 均引用 `media.assets`,但媒体上传流程(M1 后半段的 `POST /api/v1/media/uploads`)第一迭代未实现,且**对象存储供应商至今未拍板**(第一迭代决策清单 D4 遗留)。照片管理不是 M2 原文要求,纳入与否见决策 D2-1。
2. **体温记录**`pet_health` schema **没有体温表**。最接近的承载是 `health_events``measurement` 事件类型。是否需要结构化体温数据见决策 D2-4,本拆解默认不建新表。
3. **驱虫提醒**:数据模型已支持(`health_events.event_type='deworming'` + `care_reminders.reminder_type='deworming'`),属 M2 原文"提醒接口"范围内,纳入。
### 1.3 本迭代 MVP 范围(PM 建议口径,待 §4 拍板确认)
- **纳入**:宠物 CRUD(含品种目录)、pet_owners 权限校验框架、体重记录、疫苗记录(含疫苗目录、系列/剂次、scheduled→completed 状态机)、健康事件时间线、照护提醒(app 内列表,无推送)、四项聚合(最新体重、疫苗进度、下次接种、月度花费)、Flutter 档案页全量替换真实数据、两项高优先遗留埋点。
- **默认剪出(待拍板)**:照片/媒体上传、体温专表、共同照护人邀请流程(权限**校验**必须实现,邀请**交互**可后置)、提醒推送通知(通知系统属 M6)。
---
## 2. 工单列表
预估规模口径沿用第一迭代:S ≈ 半天内,M ≈ 1-2 天,L ≈ 3-5 天(含测试与文档)。
### A 组:数据与工程基础(后端)
#### T2-01 Flyway V3pet_health schema baseline 与种子数据分离
- **仓库**patbond-api(迁移脚本),patbond-doc(迁移说明)
- **描述**:从 bootstrap SQL 提取 `pet_health` 全部表结构为 Flyway V3 迁移;breeds、vaccine_catalog 的开发种子数据独立为不进生产的脚本(沿用第一迭代 identity/media 的做法)。
- **关键技术裁剪(必须遵守)**bootstrap SQL 第 1156~1166 行为 `pet_vaccinations`/`health_events``provider_id``booking_id` 增加了指向 `marketplace.providers`/`marketplace.bookings` 的外键。marketplace schema 属 M5,本迭代**不迁移**V3 必须**剥离这四条跨 schema 外键**(字段保留为裸 uuid 可空列),M5 迁移 marketplace 时再以新版本迁移补回。同理 `updated_at` 触发器依赖的公共函数需确认已在 V1 建立或随 V3 建立。
- **验收标准**
- 全新 postgres:18Testcontainers)上 V1→V2→V3 全量迁移一次成功,表结构与 bootstrap SQL 一致(跨 schema 外键除外,差异写入迁移说明)。
- 种子数据脚本与结构迁移分离,不进正式环境。
- `./mvnw clean test` 全绿(既有 82 测试不回归)。
- **依赖**:无(第一波首项)。
- **规模**M
#### T2-02 宠物健康后端模块骨架与鉴权接入
- **仓库**patbond-api
- **描述**:按开发计划 4.1 节"按迭代增加宠物健康模块;模块边界与数据库 schema 对齐",建立宠物健康业务模块(新建 Maven 模块 vs 并入现有服务见决策 D2-2,未拍板前先按 PM 建议方案搭骨架);复用第一迭代 JWT 资源侧校验,实现"当前用户"解析注入;模块只读写 `pet_health` schema。
- **验收标准**
- 模块编译入构建链,`./mvnw clean test` 全绿。
- 携带有效 access token 的请求能解析出当前用户 UUID;无 token / 过期 token 返回 401 + 既有错误码契约(40100 系)。
- Docker Compose 编排同步纳入新模块(若 D2-2 选独立服务)。
- **依赖**:D2-2 拍板(可先按建议方案开工,方案变更成本在骨架期最低)。
- **规模**M
### B 组:后端接口纵切
#### T2-03 宠物 CRUD 与 pet_owners 权限框架
- **仓库**patbond-api
- **描述**:实现 `GET/POST /api/v1/pets``GET/PATCH /api/v1/pets/{petId}` 与品种目录查询(breeds 只读列表,按 species 过滤)。创建宠物时当前用户自动成为 `pet_owners` 的 primary owner;所有 `/pets/**` 请求经统一权限校验(owner/caregiver 可写、viewer 只读、无关系 404/403,语义在契约中定死);`PATCH` 使用 `version` 乐观锁,冲突返回明确错误码;`status` 流转(active/archived 等)按数据库约束实现;软删除语义遵守 `ck_pets_deleted` 约束。
- **验收标准**
- 创建→列表→详情→更新→归档全链路走真实 PostgreSQL,重启不丢数据。
- 无权限用户访问他人宠物被拒绝(错误码与 HTTP 状态码在契约定死并有测试)。
- 并发更新(version 过期)返回明确冲突错误,有集成测试。
- breed_id 与 custom_breed_name 互斥校验(`ck_pets_breed`)应用层与数据库一致。
- **依赖**T2-01、T2-02。
- **规模**L
#### T2-04 体重记录接口
- **仓库**patbond-api
- **描述**`GET/POST /api/v1/pets/{petId}/weights`。列表 cursor 分页(`measured_at DESC, id DESC`,与既有索引对齐);创建校验 `weight_kg` 区间(>0 且 ≤500);写接口支持 `Idempotency-Key`(第 6.1 节要求)。
- **验收标准**
- 分页不丢失不重复;参数越界返回规范错误体。
- 相同 Idempotency-Key 重试不产生重复记录,有测试。
- 权限校验复用 T2-03 框架(viewer 只读)。
- **依赖**T2-03。
- **规模**M
#### T2-05 疫苗目录与疫苗记录接口
- **仓库**patbond-api
- **描述**vaccine_catalog 只读查询(按 species);`GET/POST/PATCH /api/v1/pets/{petId}/vaccinations`series_key + dose_no 唯一性(非 cancelled)、scheduled/completed/cancelled 状态机及日期约束(`ck_vaccination_dates`)、`next_due_on` 维护、`version` 乐观锁。`certificate_asset_id``provider_id``booking_id` 本迭代不开放写入(照片待 D2-1、预约属 M5),字段在契约中不出现或标记只读。
- **验收标准**
- 状态机非法迁移被拒绝并返回稳定错误码;同系列同剂次重复登记返回冲突。
- completed 必须带 administered_on、scheduled 必须带 planned_on(与数据库约束一致,应用层先行校验)。
- 集成测试覆盖成功、参数错误、不存在、无权限、并发冲突、幂等重试六类路径(第 9 节要求)。
- **依赖**T2-03。
- **规模**L
#### T2-06 健康事件时间线接口
- **仓库**patbond-api
- **描述**`GET/POST /api/v1/pets/{petId}/health-events`,建议补 `PATCH /api/v1/health-events/{eventId}`(编辑标题/备注/金额,乐观锁)。六类事件类型(medical/feeding/deworming/grooming/measurement/note);`amount_cents` 整数分(第 4.3 节);`occurred_at DESC` cursor 分页;`created_by_user_id` 记录操作者。`health_event_media` 本迭代不实现(随 D2-1)。
- **验收标准**
- 时间线分页正确;金额只收整数分且非负。
- 事件类型白名单校验与数据库约束一致。
- 六类测试路径覆盖同 T2-05。
- **依赖**T2-03。
- **规模**M
#### T2-07 照护提醒接口
- **仓库**patbond-api
- **描述**care_reminders 的列表/创建/状态流转(pending→completed/dismissedcompleted 必须写 completed_at,与 `ck_care_reminder_completed` 一致)。四类提醒类型(deworming/checkup/medication/other)。**仅 app 内数据接口,不做推送**(通知系统属 M6,见决策 D2-5)。
- **验收标准**
- 提醒可创建、按 due_at 查询待办、标记完成/忽略;状态与 completed_at 一致性有测试。
- 权限校验复用 T2-03 框架。
- **依赖**T2-03。
- **规模**M
#### T2-08 档案聚合摘要接口
- **仓库**patbond-api
- **描述**:实现档案页摘要所需聚合(建议 `GET /api/v1/pets/{petId}/summary`):最新体重、疫苗进度(completed 剂次/总剂次)、下次接种(scheduled 中最近 planned_on 或最近 next_due_on)、当月花费(health_events.amount_cents 按月求和)。全部从事实表实时聚合,**不持久化展示字符串**(第 4.3 节红线);聚合口径逐项写入契约描述。
- **验收标准**
- 各聚合值有集成测试锁定口径(含空数据、跨月边界、cancelled 疫苗不计入)。
- 时间按 `timestamptz` 存储、ISO 8601 传输,月度边界按客户端传入时区或明确定义的服务端口径(写入契约,避免歧义)。
- **依赖**T2-04、T2-05、T2-06。
- **规模**M
### C 组:契约与测试
#### T2-09 OpenAPI 契约扩展与冻结
- **仓库**patbond-doc`docs/api/openapi.yaml`),patbond-api(契约测试保证一致)
- **描述**:在既有 5 端点契约上扩展 pets 域全部端点(宠物、品种、体重、疫苗、目录、事件、提醒、摘要)。沿用既定规范:`/api/v1` 前缀、camelCase、UUID 字符串、统一信封、稳定业务错误码(pets 域新错误码段与 401/403/404/409 语义定死)、cursor 分页参数形态、`Idempotency-Key``version` 字段。**起草与 T2-03~05 并行,冻结须在 T2-03 权限/错误语义与 T2-08 聚合字段定型之后**——冻结是第三波前端联调的放行闸门(沿用第一迭代验证过的模式)。
- **验收标准**
- 契约文件评审通过;契约测试在 CI 中验证实际响应与文档一致。
- `mkdocs build --strict` 通过(契约文件更新不涉及导航变更)。
- 冻结后任何字段变更须显著上报,两端同步修改。
- **依赖**T2-03(错误/权限语义)、T2-08(聚合字段);起草仅依赖 §1.1 端点表。
- **规模**M
#### T2-10 后端集成测试滚动补齐与 CI
- **仓库**patbond-api
- **描述**:随 B 组各工单滚动补齐 Testcontainerspostgres:18)集成测试,交付前每单必须全绿(沿用第一迭代"每波 `./mvnw clean test` 必绿"纪律);V3 迁移在全新实例执行一次的校验并入 CI。本单为横切验收单,不单独排人。
- **验收标准**
- 每个业务接口覆盖成功、参数错误、资源不存在、无权限、并发冲突、幂等重试(第 9 节六类)。
- **caregiver/viewer 权限路径必须有测试覆盖**:邀请流程若按 D2-3 后置,则用测试数据直接写 pet_owners 构造三种角色场景,避免"权限代码存在但从未被验证"。
- Gitea Actions ci.yml 全绿;CI 时长若超 10 分钟记录并评估分层。
- **依赖**:随 T2-03~T2-08 滚动。
- **规模**M(分摊在各单内)
### D 组:Flutter 客户端
#### T2-11 pets feature 状态拆分与 API Client
- **仓库**patbond-flutter
- **描述**:按开发计划 4.2 节把宠物档案状态从 `AppState` 拆出独立 pets featureController → Repository → API Client 分层,对齐第一迭代 auth feature 的既有结构);依据 T2-09 冻结契约实现 DTO 与 Client(宠物、品种、体重、疫苗、事件、提醒、摘要),统一错误码解析复用既有网络层与 token 拦截。
- **验收标准**
- DTO 映射有单元测试;错误响应映射为类型化错误。
- 不再从 `AppState` 读写宠物/疫苗 demo 数据(体重、疫苗进度等展示字符串全部改为由服务端事实字段计算)。
- **依赖**:T2-09 冻结。UI 无关的分层骨架可提前与后端并行。
- **规模**M
#### T2-12 宠物列表、详情与编辑页接入真实数据
- **仓库**patbond-flutter
- **描述**:档案页(`lib/features/pets/pets_page.dart`)替换为真实列表/详情/创建/编辑:品种选择(目录接口 + 自定义品种互斥)、性别/生日/芯片号等字段对齐数据模型;**所有网络页面覆盖 loading、empty、error、retry 四态**(第 9 节硬要求);编辑冲突(409)给出明确的用户提示与刷新路径。头像照片按 D2-1 裁决处理(默认保留本地占位图,不做上传)。
- **验收标准**
- 新用户空态 → 建档 → 列表/详情展示全链路走真实后端。
- 四态齐备并有 widget 测试;乐观锁冲突提示有测试。
- **依赖**:T2-11;UI 稿可在第一波先行出设计(含四态与空态)。
- **规模**L
#### T2-13 体重与疫苗模块接入
- **仓库**patbond-flutter
- **描述**:体重录入与历史列表(分页加载);疫苗登记(选目录、系列/剂次、计划/完成状态)、疫苗进度与"下一针"改从 T2-08 摘要接口取数(替换 demo 的 `vaccines.reminderVaccine` 等本地字符串)。
- **验收标准**
- 体重与疫苗数据在另一登录设备(或清空本地数据重登)可见——对应 M2"跨设备读取"验收。
- 疫苗状态机操作的非法路径(如未填接种日期就标完成)被前端拦截且后端兜底。
- 相关 widget/单元测试补齐。
- **依赖**T2-11、T2-12。
- **规模**L
#### T2-14 健康时间线与提醒页接入
- **仓库**patbond-flutter
- **描述**:健康事件时间线(六类事件、金额录入以元展示/整数分传输、分页);照护提醒列表与完成/忽略操作;档案页"月度花费"改从摘要接口取数。demo 中硬编码的"健康提醒:已经半年没有进行体内外驱虫"改为真实提醒数据驱动。
- **验收标准**
- 时间线与提醒四态齐备;金额展示与传输换算有单元测试。
- 无提醒/无事件时空态正确。
- **依赖**T2-11、T2-12。
- **规模**M
### E 组:遗留项与收口
#### T2-15 遗留埋点:sessionId 生命周期(高优先,第一波插入)
- **仓库**patbond-flutter
- **描述**:第一迭代遗留 §1:引入 `WidgetsBindingObserver` 监听 app 前后台切换,定义会话超时与 sessionId 重建规则,修复"sessionId 只在退出时清空"的缺陷。
- **验收标准**:进后台超时回前台生成新 sessionId;规则有单元测试;不依赖 M2 契约,可与后端第一波并行。
- **依赖**:无。
- **规模**S
#### T2-16 遗留埋点:page_viewed 路由埋点(高优先,第一波插入)
- **仓库**patbond-flutter
- **描述**:第一迭代遗留 §2:接入 `RouteObserver` 挂接 `page_viewed` 事件(事件定义沿用报告 13 规范)。
- **验收标准**:主要页面路由切换产生 page_viewed 事件并能落库(复用既有 /api/v1/events 链路);有测试。
- **依赖**:无。
- **规模**S
#### T2-17 埋点:health_record_action 挂接与 M2 事件
- **仓库**patbond-flutter(挂接),patbond-doc(事件表更新)
- **描述**:第一迭代预留的 `health_record_action` 挂接点在 M2 档案功能落地后接通(建档、体重录入、疫苗登记、事件记录等动作);后端事件白名单同步扩充。埋点不得含健康敏感明文(沿用隐私红线)。
- **验收标准**:关键健康操作产生事件并落库;白名单与文档同步;有测试。
- **依赖**T2-12~T2-14(随页面落地滚动挂接)。
- **规模**S
#### T2-18 E2E 烟囱测试与真机联调收官
- **仓库**patbond-flutter(用例),patbond-apicompose 环境),patbond-doc(证据归档)
- **描述**:沿用第一迭代收官战模式:compose 起后端 → 真实链路烟囱:登录 → 建档 → 记体重 → 登记疫苗 → 记健康事件 → 摘要数值核对 → 第二账号访问该宠物被拒 → 第二设备(或重装态)同账号读到全部数据。收集 HTTP transcript(脱敏)、数据库查询证据、门禁输出。
- **验收标准**:烟囱场景全绿;契约偏差 0 个;M2 四条验收标准(跨设备、无权限拒绝、并发冲突明确、双端测试齐备)逐条有证据。
- **依赖**T2-05、T2-08、T2-13、T2-14。
- **规模**M
#### T2-19 文档与迭代收口
- **仓库**patbond-doc
- **描述**OpenAPI 定稿归档、feature-checklist 增补 M2 条目、迭代报告归档、任务板更新与收官总结。iteration-2 报告目录的 `mkdocs.yml` 导航条目由文档维护者在收口提交时统一添加(本拆解报告本身不改 mkdocs.yml)。
- **验收标准**`mkdocs build --strict` 通过;报告索引完整可还原全貌。
- **依赖**:各波交付。
- **规模**S
---
## 3. 波次划分与关键路径
沿用第一迭代验证过的"波次并行 + 契约冻结先行"模式。
### 第一波(并行开工,无互锁)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 后端数据基础 | T2-01 → T2-02 | 关键路径起点,一人连续负责 |
| 契约起草 | T2-09(起草态) | 按 §1.1 端点表 + 数据模型先出草案,随 T2-03 收敛 |
| 前端遗留埋点 | T2-15、T2-16 | 与 M2 契约零耦合,第一波消化掉两项高优先遗留 |
| UI 设计 | 档案页/四态/空态设计稿(供 T2-12) | 不占关键路径 |
### 第二波(后端纵切,契约收敛)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 后端主线 | T2-03 → T2-04/T2-05/T2-06/T2-0703 后三线可并行)→ T2-08 | T2-03 权限框架是全部业务单的前置 |
| 前端骨架 | T2-11 的分层骨架(不依赖契约的部分) | Repository/状态骨架先行 |
| 测试滚动 | T2-10 | 随各单交付即测即绿即提交 |
**波末闸门:T2-09 契约冻结**(条件:T2-03 权限/错误语义定型 + T2-08 聚合字段定型)。不冻结不放行第三波前端联调,偏差须显著上报。
### 第三波(冻结契约下两端并行)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 前端主线 | T2-11(完成)→ T2-12 → T2-13 / T2-14 | 12 完成后 13、14 可两人并行 |
| 后端旁路 | 契约测试补齐、种子数据完善、性能核对 | 不占关键路径 |
| 埋点 | T2-17 | 随页面落地滚动挂接 |
### 第四波(收官)
T2-18 E2E 烟囱 → T2-19 文档收口 → 任务板更新与验收报告。
### 关键路径
```text
T2-01 → T2-02 → T2-03(L) → T2-05(L) → T2-08 → [T2-09 冻结] → T2-11 → T2-12(L) → T2-13(L) → T2-18
```
四个 L 工单串在关键路径上,是周期决定因素。压缩手段:T2-09 草案与 T2-11 骨架前移(已排入一、二波);T2-04/06/07 走旁路不占主线;T2-12 的 UI 稿第一波先行。
---
## 4. 需要用户拍板的决策清单
以下决策 PM 只给建议,**不替用户拍板**。D2-1~D2-3 直接影响工单定稿,建议开工前优先裁决。
| # | 决策事项 | 影响 | PM 建议(仅供参考) |
| --- | --- | --- | --- |
| D2-1 | **照片/媒体是否纳入 M2**:宠物头像上传、疫苗证书、健康事件附件均依赖媒体上传流程(M1 遗留未做)与对象存储供应商(第一迭代 D4 至今未定) | 阻塞 T2-12 头像、T2-05 证书字段、health_event_media;若纳入需增补媒体上传专项工单(约 +1 L) | M2 首版**不含**照片上传,档案先跑通结构化数据;对象存储选型(建议 S3 兼容,如自建 MinIO 起步)拍板后以独立专项插入 M2 末波或 M3 |
| D2-2 | **后端模块归属**:新建独立宠物健康服务(对齐"模块边界与 schema 对齐"+ 独立部署)vs 作为模块并入 patbond-user 进程(降低双人团队运维面) | 决定 T2-02 骨架形态、compose 编排、CI 构建时长 | 新建 Maven 模块 `patbond-pet`(独立数据所有权),**部署形态倾向与 user 同进程或同 compose 独立容器均可接受**,请结合第一迭代 D2(单体 vs 多服务)一并裁决 |
| D2-3 | **共同照护人邀请流程是否入 M2**pet_owners 支持 owner/caregiver/viewer,但"邀请另一个用户"需要检索用户、发出/接受邀请等交互 | 决定 T2-03 是否扩为含邀请端点(约 +1 M)与前端邀请页 | M2 只做"创建者即 primary owner"+ 完整权限**校验**框架(测试数据覆盖三角色),邀请**交互**后置 M3+;这样 M2 验收标准"无权限用户不能访问"仍可完整验证 |
| D2-4 | **体温记录**:迭代一总结提及,但数据模型无体温表 | 若要结构化体温需新表与新迁移(超出已评审模型) | 用 `health_events``measurement` 类型承载文字化记录,不建新表;若产品明确要体温曲线图,另立数据模型变更提案再排期 |
| D2-5 | **提醒的通知形态**care_reminders 首版是否仅 app 内列表(无系统推送/本地通知) | 推送涉及通知渠道选型(platform.notifications 属 M6 | 首版仅 app 内列表 + 到期排序展示;推送后置 M6 |
| D2-6 | **breeds / vaccine_catalog 目录数据来源**:开发种子够用,但正式目录(犬猫品种表、疫苗名录)内容与量级谁提供、何时定稿 | 不阻塞开发(seed 兜底),影响上线数据质量 | 开发期用 seed(每 species 各 10~20 条常见项);正式目录数据作为独立内容任务由产品侧供稿 |
| D2-7 | **宠物删除语义**:前端提供什么入口——归档(archived)/ 软删除(deleted/ 不提供 | 影响 T2-03 状态流转范围与 T2-12 交互 | 首版仅提供"归档",软删除接口保留但前端不出入口,避免误删争议 |
| D2-8 | **中优先遗留是否纳入 M2**access token 黑名单、/internal 改 mTLS、auth_sessions 清理调优、埋点完善其余项(队列持久化等) | 纳入则挤占 M2 周期 | **不纳入**,维持技术债清单,M3 或加固阶段统一处理(TagPill 设计债同此) |
---
## 5. 遗留项插入位置汇总
| 遗留项(第一迭代总结编号) | 优先级 | 插入位置 |
| --- | --- | --- |
| 埋点 sessionId 生命周期(§1 | 高 | **T2-15,第一波**,独立工单 |
| page_viewed 路由埋点(§2 | 高 | **T2-16,第一波**,独立工单 |
| health_record_action 挂接(§7 之一) | 中(M2 天然落点) | **T2-17,第三波**随页面滚动挂接 |
| access token 黑名单(§4 | 中 | 不入 M2(待 D2-8 确认),技术债清单 |
| /internal 改 mTLS(§5 | 中 | 不入 M2(待 D2-8 确认),需先补 ADR |
| auth_sessions 清理调优(§6) | 中 | 不入 M2,性能阶段处理 |
| 埋点完善其余 4 项(§7) | 中 | 不入 M2;后端事件查询端点若 T2-17 验证需要可顺手做,超出即止 |
| TagPill 对比度设计债(§8) | 低 | 不入 M2,设计系统升级时统一处理 |
---
## 6. 风险清单
| # | 风险 | 影响 | 缓解措施 |
| --- | --- | --- | --- |
| R1 | **未提交/未推送风险**(第一迭代 R3 教训:两天工作量曾只存在于工作区) | 误操作全损;协作与 CI 失效 | 沿用已验证纪律:**每波每单交付即提交即推送**;PM 任务板每波核对三仓 `git status`。开工时基线:三仓工作区干净、与远端同步(2026-09-07 已核实) |
| R2 | **契约偏差风险**:pets 域端点数量约为第一迭代 4 倍,聚合字段口径(月度边界、进度分母)最易两端理解不一 | 联调返工 | 冻结闸门制度不放松;聚合口径在契约中逐字段写清(含时区口径);偏差显著上报,禁止任一端私改 |
| R3 | **V3 迁移照抄 bootstrap SQL 的跨 schema 外键**(第 1156~1166 行引用 marketplace) | 迁移在干净库直接失败,或被迫提前迁移 marketplace | 已写入 T2-01 描述为强制裁剪项;迁移说明记录差异与 M5 补回计划;Testcontainers 全新库验证兜底 |
| R4 | **对象存储未定拖累范围**:若 D2-1 拍板"要照片"而供应商未定 | T2-12/T2-05 范围反复 | 决策清单置顶 D2-1;默认口径按"不含照片"排期,拍板含照片则显式加 1 个 L 工单并顺延 |
| R5 | **权限路径测试盲区**:邀请流程后置时,caregiver/viewer 无自然产生入口 | "权限代码存在但从未验证",M2 验收标准落空 | T2-10 明确要求用测试数据直接构造三角色场景;E2E(T2-18)含第二账号拒绝场景 |
| R6 | **范围膨胀**:迭代一总结口径(照片/体温)比开发计划 M2 原文宽 | 周期失控、返工 | 本报告 §1.2 已逐条对照数据模型澄清;一切扩张走 §4 拍板,未拍板按 PM 建议默认剪出 |
| R7 | **CI 时长增长**:测试数将从 82 大幅增加,Testcontainers 全跑 | CI 反馈变慢、门禁被绕过 | T2-10 记录每波 CI 时长,超 10 分钟评估按模块分层执行;不降低"提交前全绿"标准 |
| R8 | **单接口面过宽的估算风险**:疫苗状态机 + 系列/剂次约束复杂度接近第一迭代 JWT 会话单 | T2-05 拖关键路径 | T2-05 已按 L 估算并置于关键路径显式管理;catalog 只读部分可先行拆出交付 |
| R9 | **文档导航遗漏**iteration-2 目录需入 mkdocs 导航,但本迭代规则限制随手改 mkdocs.yml | `mkdocs build --strict` 门禁或导航缺失 | 归入 T2-19 由文档维护者收口提交时统一处理,收口清单显式含此项 |
---
## 7. 质量要求(对全部工单生效)
- 遵守开发计划第 10 节 DoD:不依赖 Demo 常量;权限、校验、幂等、并发已处理;文档同步更新;干净环境可复现。
- 沿用既定契约规范:camelCase、UUID 字符串、ISO 8601 + timestamptz、金额整数分、统一信封与稳定错误码、cursor 分页、Idempotency-Key、version 乐观锁。
- 不提交任何密码、token、密钥;日志与埋点不含健康敏感明文与手机号全文。
- 自动化测试一律 Testcontainers postgres:18ADR-006/008);每单交付 `./mvnw clean test` / `flutter analyze` + `flutter test` 全绿。
- 所有网络页面四态(loading/empty/error/retry)齐备。
- 本迭代不实现社区、AI、预约的任何接口或页面;范围外需求记 backlog。
## 8. 工单统计
- 工单总数:**19**(数据与工程基础 2 + 后端接口 6 + 契约与测试 2 + Flutter 4 + 遗留与收口 5
- 规模分布:S × 5、M × 10、L × 4
- 关键路径长度:9 个工单(T2-01 → T2-02 → T2-03 → T2-05 → T2-08 → 冻结 → T2-11 → T2-12 → T2-13 → T2-18),其中 L × 4
- 待拍板决策:**8 项**(D2-1~D2-8,前三项建议开工前裁决)
@@ -0,0 +1,135 @@
# Patbond 第二迭代后端技术评估(Dev)
- 日期:2026-09-07
- 评估范围:patbond-api 承接 M2「宠物健康档案」的改动面、建模与 API 草案、迁移规划、遗留项耦合
- 代码基线:patbond-api `0d81c38`2026-09-04,工作区干净)
- 结论先行:**当前基线 82 个测试全绿(50.6s**;建议 M2 在 patbond-user 内以独立包实现 pet_health 域,建模跟随 patbond-doc 目标模型(体重/疫苗强结构子表 + health_events 单表),共 7 项待拍板。
## 1. 现状盘点(实际读码结论)
### 1.1 模块与代码结构
Maven 三模块:`patbond-common`(错误码/响应契约/内部 DTO)、`patbond-auth`8081,无库,Feign 调 user)、`patbond-user`8082,唯一持库服务,Flyway 归属方)。
与 M2 直接相关的既有设施,全部可复用:
- **鉴权链路**`patbond-user``BearerAuthFilter``patbond-user/src/main/java/com/patbond/patbond/user/security/BearerAuthFilter.java`)拦截 `/api/v1/*`RS256 本地验签后把 userId 放进 request attribute `patbond.authenticatedUserId`controller 用 `@RequestAttribute` 取。宠物接口直接挂在同一过滤器下,零新增鉴权代码。
- **异常/错误码契约**`ErrorCode` 枚举(common+ 每服务一个 `GlobalExceptionHandler``{code, message, data}` 信封 + 正确 HTTP 状态。扩展 = 往枚举追加值(不重编号)。
- **数据访问**:无 JPA,统一 `JdbcClient` + 手写 SQL(见 `UserRepository`),约束下沉数据库(CHECK/部分唯一索引),`updated_at` 由触发器维护。pet 域照此风格即可。
- **主键**:应用侧生成 UUIDv7`patbond-user/src/main/java/com/patbond/patbond/user/support/UuidV7.java`)。
- **埋点挂接点**`EventDictionary` 已预置 `health_record_action`props 白名单 `recordType`/`actionType`),M2 后端无需改埋点代码,Flutter 侧触发即可。
- **测试设施**TestcontainersPostgreSQL 18+ `TestcontainersConfiguration`,集成测试模式成熟,pet 域测试直接套用。
### 1.2 数据库现状
Flyway 链在 patbond-userV1identity/media/platform 基线)+ V2platform.product_events)。**pet_health schema 尚未创建**V1 只建了 platform/identity/media 三个 schema)。开发种子在 `db/dev/afterMigrate__dev_seed.sql`,默认不执行。
patbond-doc 目标模型(`patbond-doc/docs/database/patbond_postgresql.sql` 333-560 行)已给出完整的 pet_health 设计,共 8 张表:`breeds``pets``pet_owners``pet_weight_records``vaccine_catalog``pet_vaccinations``health_events``health_event_media``care_reminders`。该模型已经过评审,M2 建模应以它为正典裁剪,而不是另起炉灶。
### 1.3 缺口
- **media 上传流程未实现**:仓库中没有任何 media 相关代码(无 controller/service),`media.assets` 只有表。目标模型中宠物头像、疫苗证书、健康事件附件全部 FK 到 `media.assets`——附件能力被 media 上传流程阻塞(见待拍板 P3)。
- `marketplace` schema 未建:目标模型中 `pet_vaccinations.provider_id/booking_id``health_events.provider_id/booking_id` 本就未设 FK(预留列),M2 保留可空列即可,无阻塞。
## 2. 改动面评估
| 改动面 | 内容 | 量级 |
| --- | --- | --- |
| Flyway | V3 pet_health 结构基线(从目标模型裁剪)+ V4 字典种子(若拍板引入) | 中 |
| 新代码 | pet 域 controller/service/repository/DTO(约 5 组资源) | 大(M2 主体) |
| common | `ErrorCode` 追加 3~4 个值;若拍板新模块则需下沉 `UuidV7`/`BearerAuthFilter` | 小 |
| 契约 | openapi.yaml 冻结新增 pets 相关 path(实现前先冻结,本评估不动契约) | 中 |
| 既有代码 | 零改动(鉴权过滤器、异常处理、埋点均直接复用) | — |
| 依赖 | **无需新增任何依赖**JdbcClient + Flyway + Testcontainers 足够,不引 JPA | — |
## 3. 领域建模草案
### 3.1 模块归属【待拍板 P1】
- **方案 A:新建 `patbond-pet` Maven 模块(独立服务)**。符合开发计划 4.1「按迭代增加模块,边界与 schema 对齐」的字面方向。代价:新端口/compose 服务/CI 矩阵;`BearerAuthFilter``JwtVerifier``GlobalExceptionHandler``UuidV7` 需下沉 common 或复制;Flyway 单链归属要拆(共库单 `flyway_schema_history`,需为新模块配独立 history 表),部署与联调面翻倍。
- **方案 B(推荐):在 patbond-user 内新增独立顶层包 `com.patbond.patbond.user.pethealth`**。零基础设施成本,Flyway 链自然延续(V3+),鉴权/异常/UUIDv7 直接复用。约束:包内不 import user 域内部类(只经 service 接口),SQL 只碰 `pet_health` schema(读 `identity.users` 仅限权限校验 join),保证未来抽成独立模块时是「搬包 + 拆迁移」而非重写。
- 推荐 B:MVP 单实例共库阶段,「模块边界与 schema 对齐」用包边界 + schema 读写纪律即可兑现,把工程成本留给业务代码。
### 3.2 表结构草案(Flyway V3,从目标模型裁剪)
按目标模型原样建(列、CHECK、部分唯一索引、`set_updated_at` 触发器全保留),仅做以下裁剪调整:
| 表 | M2 处置 | 调整点 |
| --- | --- | --- |
| `pets` | 建 | 主键去掉 `DEFAULT gen_random_uuid()`,应用侧 UUIDv7(与 users 做法对齐);`avatar_asset_id` 保留可空列(media 未实现,暂不写入) |
| `pet_owners` | 建 | 目标模型原样;创建宠物时自动写入 `(pet_id, creator, 'owner', is_primary=true)` |
| `pet_weight_records` | 建 | 原样 |
| `breeds` + `vaccine_catalog` | 建(P2 拍板) | 若引入:结构进 V3、种子进 V4 正式迁移(字典是生产数据,不放 db/dev);若不引入:pets 全走 `custom_breed_name``ck_pets_breed` 约束允许),疫苗表需把 `vaccine_id` 放宽为自由文本——**偏离目标模型,后续迁移代价大** |
| `pet_vaccinations` | 建 | `provider_id`/`booking_id`/`certificate_asset_id` 保留可空预留列,M2 不写入 |
| `health_events` | 建 | `event_type` 枚举沿用目标模型 6 值(medical/feeding/deworming/grooming/measurement/note |
| `health_event_media` | **不建,推迟** | 依赖 media 上传流程(P3);纯增量表,后续 V5+ 补零成本 |
| `care_reminders` | 建(P6 拍板) | 纯 CRUD,无推送 |
与 user/auth 的关系:`pet_owners.user_id -> identity.users(id)``health_events.created_by_user_id -> identity.users(id)` 两个跨 schema FK,共库阶段保留(与 V1 中 `media.assets.owner_user_id` 先例一致)。鉴权只用 JWT 里的 userId,不新增 auth 侧改动、不新增 `/internal` 接口。
### 3.3 健康记录类型建模:单表 + type vs 每类型子表【已由目标模型定调,确认即可】
- 纯单表(所有记录一张表 + type + jsonb):查询简单,但体重/疫苗的强约束(剂次唯一、状态-日期一致性、数值范围)全丢给应用层。
- 纯子表(每类型一张表):表爆炸,时间线聚合要 UNION 多表。
- **推荐(= 目标模型的混合方案)**:`pet_weight_records``pet_vaccinations` 独立强结构子表(各自的 CHECK 与部分唯一索引是业务规则本体,如「同系列同剂次未取消唯一」);其余低结构记录统一进 `health_events` + `event_type` 枚举。时间线视图由 health_events 承载,体重/疫苗页各查各表。
## 4. API 资源设计草案(供契约冻结参考,本评估不改 openapi.yaml
路径与开发计划 6.2 对齐,全部挂 `BearerAuthFilter` 强制鉴权:
| 接口 | 说明 |
| --- | --- |
| `GET /api/v1/pets` | 当前用户可见宠物列表(经 pet_owners join);量小,建议一次性返回不分页(契约冻结时定) |
| `POST /api/v1/pets` | 创建,创建者自动 primary owner,返回 201 |
| `GET /api/v1/pets/{petId}` | 详情(含调用者自己的 role) |
| `PATCH /api/v1/pets/{petId}` | 更新,请求体带 `version` 乐观锁,冲突返回 409/40902 |
| `DELETE /api/v1/pets/{petId}` | 软删(status=deleted),仅 owner;是否进 M2 契约冻结时定 |
| `GET/POST /api/v1/pets/{petId}/weights` | 体重记录(GET 按 measured_at 倒序,cursor 分页) |
| `GET/POST /api/v1/pets/{petId}/vaccinations``PATCH .../vaccinations/{id}` | 疫苗记录(PATCH 带 version;状态迁移 scheduled→completed/cancelled |
| `GET/POST /api/v1/pets/{petId}/health-events` | 健康时间线(cursor 分页:`(occurred_at, id)` 复合游标,与既有索引对齐) |
| `GET/POST/PATCH /api/v1/pets/{petId}/reminders` | 提醒 CRUDP6 |
| `GET /api/v1/pets/{petId}/health-summary` | 服务端聚合:最新体重与趋势、疫苗进度、下次接种、当月花费(P5) |
| `GET /api/v1/breeds``GET /api/v1/vaccines` | 字典只读接口(若 P2 拍板引入;query 参数 species |
权限规则:owner 全权;caregiver 可读写记录、不可改宠物档案与成员;viewer 只读。M2 只实现 owner 路径(P4),但 repository 层权限查询按三档写好。
错误码扩展(追加进 `ErrorCode`,延续现有编号段):
| code | HTTP | 语义 |
| --- | --- | --- |
| 40300 `PET_ACCESS_DENIED` | 403 | 对可见宠物无相应操作权限(如 viewer 尝试写) |
| 40401 `PET_NOT_FOUND` | 404 | 宠物不存在**或调用者不可见**(防 ID 枚举,见 P7) |
| 40402 `RECORD_NOT_FOUND` | 404 | 宠物下的记录不存在 |
| 40902 `VERSION_CONFLICT` | 409 | 乐观锁版本冲突(对应验收标准「并发更新返回明确冲突」) |
幂等:开发计划 6.1 的 `Idempotency-Key` 强制名单(帖子/生成任务/预约)不含 pets,M2 写接口不强制幂等键;客户端重试语义靠乐观锁 + 唯一约束兜底。
## 5. 待拍板清单
| # | 事项 | 选项 | 推荐 |
| --- | --- | --- | --- |
| P1 | 模块归属 | 新建 patbond-pet 模块 vs patbond-user 内独立包 | user 内独立包(3.1) |
| P2 | 品种/疫苗字典 | 引入 breeds + vaccine_catalogV3 结构 + V4 种子)vs 自由文本 | 引入字典,种子最小集(犬猫核心疫苗),避免偏离目标模型 |
| P3 | 附件/图片 | 进 M2(需先实现 media 上传流程)vs 推迟 | **推迟出 M2**;media 上传是独立工作量,不该给健康档案当前置;表列已预留 |
| P4 | 共同照护人 | 邀请/成员管理 API 进 M2 vs 只做 owner 自动归属 | 只做 owner,权限校验按三档 role 实现好,邀请 API 下迭代 |
| P5 | 健康汇总聚合 | 服务端 `health-summary` 接口 vs 客户端自聚合 | 服务端聚合(计划 M2 验收提到「从事实表聚合生成」,且跨设备一致) |
| P6 | 提醒 | care_reminders CRUD 进 M2(无推送)vs 推迟 | 进 M2 做纯 CRUD(计划 M2 范围明确包含),推送依赖通知基础设施、明确不做 |
| P7 | 无权限读取语义 | 403 vs 404 | 不可见宠物一律 404/40401(防枚举);可见但越权操作 403/40300 |
## 6. 遗留中低优先项与 M2 的耦合评估
- **access token 黑名单**:与 M2 **弱耦合,建议不进本迭代**。宠物权限每次请求实时查 `pet_owners`,撤销照护关系立即生效,不依赖 token 吊销;access token 15 分钟 TTLADR-003)对健康档案的敏感级别足够。黑名单需求真正的触发点是「改密/封号即时踢出」,属身份域主题,与 pet 域实现无交集。
- **/internal 改 mTLS**:与 M2 **无耦合,建议不进本迭代**。M2 不新增任何 `/internal` 接口(按 P1 推荐方案,pet 域与 user 同进程,连内部调用都没有);即使 P1 拍板为独立模块,也应沿用现有静态 service token 方案,mTLS 留给微服务化阶段(与 ADR-002 的节奏一致)。
## 7. 构建与测试基线(2026-09-07 实测)
命令:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw test`(系统默认 JDK 26 不可用于构建,须显式指定)。
| 模块 | 测试数 | 结果 |
| --- | --- | --- |
| patbond-common | 3 | 通过 |
| patbond-user | 48 | 通过(含 Testcontainers 集成测试) |
| patbond-auth | 31 | 通过 |
| **合计** | **82** | **全绿,BUILD SUCCESS,总耗时 50.6s** |
与第一迭代收官基线(82 测试)一致,无回归。此为 M2 开工基线:M2 结束时测试数只增不减,且该命令保持一次通过。
@@ -0,0 +1,210 @@
# 03 · Flutter 前端技术评估(M2:宠物健康档案)
> 作者:Frontend Developer
> 日期:2026-09-07
> 依据:第一迭代收官报告(iteration-1/08、12、13 号)、ADR-005 珊瑚橙正典、`patbond-doc/docs/api/openapi.yaml`
> 性质:开工前评估,只读分析 + 验证性测试,未改动任何生产代码。
## 0. 基线验证
```text
$ flutter test # patbond-flutter @ Flutter 3.44.6 stable
00:03 +34: All tests passed! # 34 个测试全绿,与第一迭代收官记录一致
```
测试分布:auth_repository 10、token_refresher 5、login_page 4、register_page 4、analytics_service 4、app_text_field 3、primary_button 3、widget_test(导航冒烟)1。**M2 以 34 为基线**,收官时只增不减。
## 1. 现状盘点(实际读码结论)
### 1.1 路由结构
- **无命名路由、无 go_router**。根路由是 `app.dart` 里基于 `SessionManager.status``AnimatedSwitcher`(Splash ↔ 登录 ↔ 主壳,300ms fade),不走 Navigator。
- 主壳 `main_shell_page.dart``IndexedStack` + `NavigationBar` 承载 5 个 Tab(首页/创作/**档案**/服务/我的),Tab 切换是 `setState`**不产生路由事件**。
- 二级页用 `Navigator.push``MaterialPageRoute``core/navigation/fade_route.dart``fadePageRoute`),目前**都没有传 `RouteSettings.name`**。
- `MaterialApp` 目前没有挂任何 `navigatorObservers`——RouteObserver 是空白,正好是遗留项 2 的落点。
### 1.2 状态管理与数据层
- 模式统一为 **ChangeNotifier + 构造器注入**,无第三方状态库:`AppState`demo 数据 + shared_preferences 持久化)、`SessionManager`(认证状态机 + flutter_secure_storage)。页面通过 `ListenableBuilder`/`AnimatedBuilder` 订阅。
- 网络层已完备:`ApiClient.request()`(错误信封 → 类型化异常;401/40101 单飞刷新后重放一次;429 → `ApiRateLimitException`)、`AuthInterceptor``requiresAuth` extra 标记 + `X-Device-Id`)。**健康档案接口可直接复用,无需动网络层**。
- 仓储模式已确立:`AuthRepository` 抽象接口 + `ApiAuthRepository` 实现,widget 测试注入假实现。健康档案照此办理即可。
- 模型全部手写 `fromJson/toJson``lib/models/models.dart`),无 codegen。已有 `PetProfile` / `VaccineRecord` / `VaccineItem`,但它们是 **demo 数据形态**(如 `birthday` 为字符串、无服务端 id),对接后端契约时需要新建模型而非硬改。
### 1.3 档案 Tab 现状(M2 主改造对象)
`features/pets/pets_page.dart`724 行)目前完全跑在 `AppState` demo 数据上:宠物资料卡 + 疫苗进度 + 硬编码的「成长足迹」时间线(两条写死的 `_TimelineTile`)+ 硬编码健康提醒/本月花费。编辑走 `showModalBottomSheet``EditPetSheet` / `VaccineSheet`),表单校验是 **SnackBar 弹错的旧模式**,未用登录纵切确立的 errorText 受控模式。M2 的健康档案页面族基本等于**重写这一 Tab 及其下钻页**。
### 1.4 Analytics 模块现状(与 13 号规范有实质偏差,须在 M2 修正)
实际读 `lib/analytics/analytics_service.dart` 与装配代码,发现四处与 13 号埋点规范 / 收官记录不一致:
| # | 规范/记忆中的状态 | 代码实际状态 | 位置 |
| --- | --- | --- | --- |
| 1 | shared_preferences 分段队列、500 条上限、指数退避 | **纯内存队列**,攒 20 条上传一次,失败整批丢弃(M0 简化注释自认) | `analytics_service.dart:18-25` |
| 2 | `eventId` 用 UUIDv7 | 用 `Uuid().v4()` | `analytics_service.dart:50` |
| 3 | `sessionId` 冷启动/后台 30 分钟重生成 | **每个事件随机生成一个 v4**(注释标注 M0 简化) | `analytics_service.dart:55` |
| 4 | 5 个挂接点已挂 3 个 | `ApiAuthRepository` 里 login/register/logout 挂接点齐全(成功/失败共 5 处 track),**但 `app.dart` 组装时根本没传 analytics 实例**——生产构建里 `_analytics` 恒为 null,**埋点实际未接线** | `app.dart:46-50``auth_repository.dart:38,45` |
另有两处小问题顺带记录:`appVersion` / `osVersion` 是硬编码字符串(TODO 注 package_info_plus / device_info_plus);`Platform.isAndroid` 判断在 web/桌面上会抛(当前只出 mobile 包,暂不阻塞)。
**结论**:两个遗留项(sessionId 生命周期、page_viewed)落地前,必须先把 AnalyticsService 在 `app.dart` 接线,否则做了也是空转。接线本身改动极小(见 §3.1 清单第 3 条)。
### 1.5 主题与设计债
主题体系健康:语义 token`AppColors`/`AppRadius`)集中于 `app_theme.dart`,健康档案新页面直接引用 token 即可,无需扩色板;健康类语义色(`success`/`successInk`/`successSurface` sage 系)现成可用。DEBT-1TagPill 11px sage 文字对比约 2.4:1)在档案时间线的状态标签处会**高频复现**——健康档案是 TagPill 密度最高的页面族,建议 M2 内一并偿还(方案归 UI Designer 定,候选:文字换 `successInk`、或加深底色;前端改动约 1 处组件 + 全局回归)。
### 1.6 契约依赖(阻塞项)
`openapi.yaml` 当前只有 5 条 auth/me 路径,**尚无任何 pet/health 接口**。健康档案前端开发严格依赖契约冻结先行(延续第一迭代流程)。前端可先行的部分:两个遗留埋点项、页面骨架/空态/表单 UI、模型与仓储接口留假实现。
## 2. 健康档案页面族方案草案
### 2.1 页面与路由规划
沿用「档案 Tab 为入口、Navigator.push 下钻」的现有结构,不引入新路由框架(权衡见 §4-A):
| 页面 | 形态 | 路由名(供 page_viewed | 说明 |
| --- | --- | --- | --- |
| 档案首页(重构 PetsPage | Tab 页 | `pet_archive`(Tab 视图名) | 宠物资料卡 + 健康概览 + 健康记录时间线(倒序、按类型图标区分),替换现硬编码内容 |
| 健康记录列表(如首页时间线只展示近 N 条) | push | `/health/records` | 全量时间线,支持按类型筛选;列表即时间线,**不做独立列表页与时间线两套 UI** |
| 记录详情 | push | `/health/record` | 只读展示 + 编辑/删除入口 |
| 新增/编辑记录表单 | push 全屏页 | `/health/record/edit` | 类型(疫苗/驱虫/体检/就诊/体重…按契约枚举)、日期、标题/机构、备注、数值字段随类型联动 |
| 宠物资料编辑 | 保留 bottom sheet | sheet 不计路由曝光) | 沿用 `EditPetSheet` 交互形态,校验改造为 errorText 受控模式 |
要点:
- 新增/编辑用**全屏 push 页而非 bottom sheet**:健康记录字段多于宠物资料,sheet 内长表单 + 键盘 + 校验错误的可用性差;也让 page_viewed 能自然覆盖(权衡见 §4-B)。
- 所有 `Navigator.push` 从 M2 起**必须传 `RouteSettings(name: ...)`**,这是 page_viewed 的取数来源(§3.2)。
- 目录按现约定放 `lib/features/health/``health_models.dart``health_repository.dart``health_store.dart``pet_archive_page.dart``record_detail_page.dart``record_edit_page.dart`
### 2.2 状态管理与数据层(沿用现有模式,零新依赖)
```
HealthRepository(抽象接口)
Future<PetDetail> getPet();
Future<List<HealthRecord>> listRecords({RecordType? type});
Future<HealthRecord> createRecord(HealthRecordDraft draft); // Idempotency-Key: uuid.v4(沿用注册的幂等键模式)
Future<HealthRecord> updateRecord(String id, HealthRecordDraft draft);
Future<void> deleteRecord(String id);
ApiHealthRepository implements HealthRepository // ApiClient.request(..., requiresAuth: true)
HealthStore extends ChangeNotifier // 列表/宠物数据 + 加载状态机,页面 ListenableBuilder 订阅
```
- `HealthStore` 持一个显式加载状态机 `idle → loading → ready / empty / failed`,替代 `AppState.isReady` 那种单布尔(列表页需要区分空态与失败态)。
- 装配处在 `app.dart``_buildRepository()` 同层:复用同一个 `ApiClient` 实例,`MainShellPage` 构造器注入 store;测试注入 `FakeHealthRepository`(复刻 auth 测试的注入手法,`test/helpers/` 已有先例)。
- 模型手写 JSON(延续现约定,不引 codegen);字段名以冻结后的契约为准,**不复用 demo 形态的 `PetProfile`/`VaccineRecord`**demo 模型与 `AppState` 中对应字段在档案 Tab 重构完成后择机下线。
- 错误处理复用类型化异常分层,与登录纵切一致:`ApiBusinessException` 按 code 映射字段级/表单级文案;`SessionExpiredException` 由状态机自动送回登录页(无需页面处理);`ApiNetworkException` → SnackBar + 重试;`ApiRateLimitException` → 表单级横幅。
### 2.3 表单校验(复用登录纵切的 errorText 受控模式)
`record_edit_page.dart` 逐条复刻 `login_page.dart` 已验证的模式:
- 每字段一个 `String? _xxxError` state + `AppTextField(errorText: ...)`
- blur 校验:`Focus(onFocusChange: (has) { if (!has) _validateXxxOnBlur(); })`
- 输入即清错:`onChanged` 里清本字段错误与表单级横幅;
- 提交前全量校验,服务端字段级错误(如契约给出 422 字段错误)映射回对应 `errorText`,业务级错误走 `InlineErrorBanner` + `SemanticsService.sendAnnouncement`(无障碍播报,登录页已有先例);
- 提交中 `PrimaryButton(isLoading: true)` + 字段 `enabled: !_submitting`
- 非文本控件(日期、类型选择)错误提示:`AppTextField` 之外的控件没有 errorText 通道,用控件下方 12px `AppColors.error` 辅助文案行,样式对齐 `errorStyle`
同时把 `EditPetSheet` / `VaccineSheet` 的 SnackBar 弹错**改造为同一模式**,消除仓库内两套校验风格并存。
### 2.4 加载 / 空态 / 离线
| 态 | 处理 |
| --- | --- |
| 加载 | 首屏 `CircularProgressIndicator`(复用主壳 isReady 的样式);M2 不做骨架屏(页面族小,收益低) |
| 空态 | 无任何健康记录:插画位(爪印 Icon + `surfaceTint` 底)+ 引导文案 + 「记录第一条」CTA 直达新增表单 |
| 失败 | 列表加载失败:页内错误态 + 重试按钮(复刻 Splash 失败态版式);操作失败按 §2.2 错误分层 |
| 下拉刷新 | `RefreshIndicator` 包列表,成功静默、失败 SnackBar |
| 离线 | M2 推荐**只读缓存**:列表成功响应 JSON 落 shared_preferences(非敏感数据,符合 13 号规范的存储红线),冷启动/断网先渲染缓存并标注「展示的是上次同步数据」,后台刷新成功后替换;**写操作不做离线排队**(冲突处理复杂度不匹配 M2 体量),断网提交直接走网络错误分层。权衡见 §4-C,待拍板 |
## 3. 两个遗留高优先项:实现方案与改动点清单
两项都建议排在 **M2 第一波**(不依赖健康契约冻结,可与契约评审并行),且共享前置:把 AnalyticsService 在 `app.dart` 接线(§1.4 #4)。
### 3.1 sessionId 生命周期(WidgetsBindingObserver
**方案**(对齐 13 号规范 §3.1 `session_tracker.dart` 设计):
新建 `lib/analytics/session_tracker.dart`
```dart
class SessionTracker with WidgetsBindingObserver {
// 冷启动:构造时生成 sessionId = Uuid().v7()
// didChangeAppLifecycleState:
// paused/inactive → 记 _lastPausedAt(内存即可,进程死了本来就是冷启动)
// resumed → 距 _lastPausedAt 超 30 分钟则重新生成 sessionId
String get sessionId;
}
```
- 30 分钟阈值做成构造参数(默认 30min),时钟做成 `DateTime Function() now` 注入,测试免等待。
- `lastActiveAt` 落不落 shared_preferences:规范原文要求持久化(`pb.analytics.lastActiveAt`),但其唯一作用是跨进程判定,而**冷启动本来就必然换新 sessionId**,持久化无增量价值——建议**不持久化,纯内存**(偏离规范一处,需数据侧确认,待拍板 §4-D)。
**改动点清单**
| # | 文件 | 改动 |
| --- | --- | --- |
| 1 | `lib/analytics/session_tracker.dart` | 新建(约 40 行) |
| 2 | `lib/analytics/analytics_service.dart` | 构造器增加 `String Function() getSessionId`;删除 `'sessionId': const Uuid().v4()` 改为调用注入的 getter;顺手把 `eventId``v4()``v7()`uuid ^4.6.0 原生支持,对齐规范) |
| 3 | `lib/app/app.dart` | `initState` 实例化 `AnalyticsService` + `SessionTracker``WidgetsBinding.instance.addObserver(tracker)``_buildRepository()` 把 analytics 传入 `ApiAuthRepository`**修复未接线**);`dispose` removeObserver |
| 4 | `test/analytics/session_tracker_test.dart` | 新建:冷启动生成、resume<30min 不变、resume≥30min 重生成、连续 pause/resume 幂等(`TestWidgetsFlutterBinding.handleAppLifecycleStateChanged` 驱动 + 注入假时钟) |
| 5 | `test/analytics/analytics_service_test.dart` | 现有 4 测试补断言:同一 tracker 下多事件 sessionId 相同 |
### 3.2 page_viewed 路由埋点(RouteObserver
**方案**`NavigatorObserver` 派生类而非 `RouteObserver<PageRoute>` + RouteAware(后者要求每个页面 State mixin RouteAware 并注册/注销,N 个页面 N 处样板;前者集中一处、页面零侵入。权衡见 §4-E)。
新建 `lib/analytics/analytics_route_observer.dart`
```dart
class AnalyticsRouteObserver extends NavigatorObserver {
// didPush / didPop / didReplace:取 route.settings.name
// 非空且非 sheet/dialogroute is PageRoute)才 track('page_viewed', {'pageName': name, 'previousPageName': ...})
// didPop 上报的是「回退后重新曝光的前一页」
}
```
覆盖三类非 Navigator 的「页面曝光」需手动补点(这是本仓库路由结构的特殊性,纯 RouteObserver 覆盖不到):
1. **主壳 Tab 切换**IndexedStack 无路由事件):`MainShellPage.selectTab` 内 trackTab 名映射 `home / create / pet_archive / services / profile`;初始 Tab 在 `initState` 补一次。
2. **认证状态机切页**(根部 AnimatedSwitcher 无路由事件):Splash/登录/主壳的切换在 `app.dart``_homeForStatus` 分支处补点(或仅对 login 页补,待拍板颗粒度)。
3. bottom sheet 不计入 page_viewed(与 §2.1 约定一致)。
`page_viewed` 是**事件字典 v1(11 个 auth 事件)之外的新事件**,需要在 13 号字典追加条目(`eventVersion: 1`,属性:`pageName``previousPageName`、可选 `source`: `push/pop/tab/auth_switch`),字典变更须经数据侧确认——前端不擅自开报。
**改动点清单**
| # | 文件 | 改动 |
| --- | --- | --- |
| 1 | `lib/analytics/analytics_route_observer.dart` | 新建(约 50 行) |
| 2 | `lib/app/app.dart` | `MaterialApp(navigatorObservers: [analyticsRouteObserver])` |
| 3 | `lib/features/main/main_shell_page.dart` | 注入 analytics(或回调);`selectTab` + `initState` 补 Tab 曝光点 |
| 4 | 现有全部 `Navigator.push` 调用点(`main_shell_page.dart` openPost、`login_page.dart` _goRegister、`fade_route.dart` 签名加可选 settings | 补 `RouteSettings(name: ...)`;M2 新页面从第一天就带 name |
| 5 | `patbond-doc` 13 号字典 | 追加 `page_viewed` 条目(数据侧评审后) |
| 6 | `test/analytics/analytics_route_observer_test.dart` | 新建:push/pop/无名路由不报/sheet 不报;Tab 切换补点在 shell 冒烟测试中断言 |
**注意**:两项落地后事件量将从「每会话 <10 条」上升(page_viewed 是高频事件),§1.4 #1 的内存队列(失败整批丢弃)会放大数据丢失。建议把 13 号规范的 **shared_preferences 分段队列**列入 M2 第二波(不阻塞两个遗留项,但应在健康档案功能埋点铺开前就位)。
## 4. 权衡与待拍板
| # | 议题 | 选项 | 推荐 |
| --- | --- | --- | --- |
| A | 路由框架 | ① 维持 Navigator 1.0 + push;② 引入 go_router | **①**。页面族仅 3 个下钻页,无 deep link 需求;go_router 迁移波及登录纵切已验证的 AnimatedSwitcher 认证切换结构,风险收益不匹配。deep link 需求出现时(推送直达记录详情)再评估 |
| B | 新增/编辑表单形态 | ① 全屏 push 页;② bottom sheet(与 EditPetSheet 一致) | **①**。字段多 + 键盘 + errorText 校验在 sheet 内可用性差,且 sheet 不产生路由事件、埋点需再补点。代价:与宠物资料编辑(保留 sheet)形态不一,需 UI Designer 认可 |
| C | 离线策略 | ① 纯在线 + 失败重试;② 只读缓存最近列表;③ 完整离线(写排队+冲突解决) | **②**。成本约一个缓存读写封装,显著改善弱网首屏;③ 明确出 M2 范围 |
| D | sessionTracker 的 lastActiveAt 是否持久化 | ① 按规范落 prefs;② 纯内存 | **②**(理由见 §3.1)。属对 13 号规范的偏离,需数据侧点头 |
| E | page_viewed 采集机制 | ① NavigatorObserver 集中式;② RouteObserver + RouteAware 分布式 | **①**。零页面侵入、单点测试;②仅在需要「页面 resume 时长统计」时更优,当前事件不含时长 |
| F | 埋点队列升级时机 | ① M2 第二波做分段队列;② 推 M3 | **①**(理由见 §3.2 注意),且 `EventQueue` 接口规范里已设计好,实现面可控 |
| G | DEBT-1TagPill 对比度) | 修复方案归 UI Designer | 建议纳入 M2(§1.5),健康档案是 TagPill 最密页面 |
## 5. 风险与依赖小结
1. **契约冻结是关键路径**openapi.yaml 尚无 health 接口;第一波先做两个埋点遗留项 + 表单/空态骨架可完全并行。
2. **埋点未接线**(§1.4 #4)是收官记录与代码的最大出入,接线动作已并入 §3.1 清单第 3 条,成本极低但必须做。
3. 档案 Tab 重构会触碰 `AppState` demo 数据的退役边界(pet/vaccines 字段),首页问候卡、主壳头像也引用 `appState.pet`——重构时需全局 grep 引用面,避免半迁移状态。
4. 本评估未改任何生产代码;测试基线 34 全绿已复验。
---
**Frontend Developer** · 2026-09-07
@@ -0,0 +1,139 @@
# 04 · M2 开工前现实核查(Reality Check · 复核版 v2
- 核查人:Reality CheckerTestingRealityChecker,正式接管复核)
- 日期:2026-09-07(复核);初版同日由通用核查人代写,本版为逐条重验后的接管版
- 方法:**不采信任何书面转述**。所有结论分三档标注——【亲验】命令自己跑、输出自己看;【UNVERIFIED】本地无法复现、明确不采信;【勘误】初版或同伴报告与实测不符之处
- 约束遵守:只读核查 + 运行测试/构建/匿名 API 查询;零生产代码改动、零 commit/push、未改 mkdocs.yml
---
## 0. 裁定(先说结论)
**M2 开工 readinessCONDITIONAL PASS(附条件放行)。**
测试与 CI 基线的证据是压倒性的且全部由本人亲验:后端 82/82、前端 34/34、flutter analyze 0 问题、mkdocs strict 通过、三仓 HEAD 的 CI 状态经 Gitea commit status API 亲查全为 success。代码仓(api/flutter)工作树干净且与远端一致。
不给 CERTIFIED 的理由:①patbond-doc 工作树当前**不干净**(第一迭代最高风险模式的复发苗头,见 §1 勘误);②D-1 契约缺口属实且因埋点接线问题而升级;③**生产 App 埋点整体空转**(亲验坐实,见 §3.1);④E2E 通道状态 UNVERIFIED。放行条件见 §5。
---
## 1. 初版七项核查的逐条复验
### RC-1 三仓 Git 状态 — 【亲验,**部分勘误**】
`git status --porcelain` + `git rev-list --count @{u}..HEAD` 逐仓实测(2026-09-07):
| 仓库 | 分支 | 工作树 | 未推送 | 本地=远端 HEAD |
| --- | --- | --- | --- | --- |
| patbond-api | dev | 干净 | 0 | `0d81c38` ✓ |
| patbond-flutter | dev | 干净 | 0 | `3f8388e` ✓ |
| patbond-doc | main | **不干净** | 0(已提交部分) | `5537f92` ✓ |
**勘误(初版 RC-1 与 08 号报告的「三仓干净」已过时)**patbond-doc 当前有 `mkdocs.yml` 未提交修改(挂载第二迭代 8 份报告的导航)+ `docs/development/iterations/iteration-2/` 整目录(8 份开工报告)未跟踪。这些是本波次自产内容而非第一迭代残留,但**8 份开工报告 + 导航变更全部未提交、未推送**——这正是第一迭代教训里「文档长期不 commit」的同款模式,列为放行条件 1。
### RC-2 后端测试基线 — 【亲验属实,**初版计数勘误**】
命令:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw test`(本人重跑,BUILD SUCCESS35.6s0 失败 0 错误 0 跳过)。
- 逐模块汇总行实测:common **3**、user **48**、auth **31**,合计 **82**,与「82 全绿」声称一致。
- **勘误**:初版写「user 47」且「3 + 47 + 31 = 82」——3+47+31=81,算术不自洽;实测 user 模块汇总行为 `Tests run: 48`(初版自己罗列的 10 个测试类之和 5+1+7+13+5+3+3+1+7+3 也是 48)。结论未变,但这种笔误正是不能采信书面数字的例证。
- Testcontainers 正常(集成测试全过即为 Docker 可用的实证)。
### RC-3 前端测试基线 — 【亲验属实】
`flutter test` 本人重跑:`00:05 +34: All tests passed!`34/34。另补跑 `flutter analyze`**No issues found**0.7s),与 `3f8388e` 提交声称的「analyze 清零」一致。
### RC-4 文档构建 — 【亲验属实】
`mkdocs build --strict` 本人重跑:通过(0.67s)。注意本次是在**已挂 iteration-2 导航的未提交 mkdocs.yml** 下通过的——即当前未提交导航不破坏门禁,提交后 CI docs-build 预期同样能过。mkdocs 1.6.1pip user 安装 / Python 3.14)实测在位;本地 pip 与 CI apt 渠道不同的版本漂移注意项维持有效。
### RC-5 OpenAPI 契约缺口 D-1 — 【亲验属实,严重度上调理由见 §3.1】
`docs/api/openapi.yaml` 全文 grep `events`**0 命中**。契约仅 5 端点:`/api/v1/auth/register`(:54)、`/login`(:86)、`/refresh`(:126)、`/logout`(:159)、`/me`(:188)——行号与初版一致。`POST /api/v1/events` 已实现、已测(AnalyticsIntegrationTest 7 用例在本次 82 里全绿)却游离于契约之外,**D-1 属实**。
### RC-6 环境事实 — 【亲验属实】
JDK 17.0.20.1、Docker Server 29.7.2、Flutter 可用(test+analyze 实跑)、mkdocs 1.6.1——均本人实测。初版「CI runner 本地不可核实」一条**已被 §2 的亲验取证取代**。
### RC-7(初版)E2E 7/7 — 【**UNVERIFIED**,明确不采信】
真机联调 E2E 7/7 与「契约偏差 0」依赖起双服务 + 真机,本地无法复现,本人未取得任何一手证据。初版用词「书面采信」,本版改判 **UNVERIFIED**:该结果只代表 2026-09-04 收官时点,通道今日是否仍活没有证据。列为放行条件 3。
---
## 2. CI 状态取证(初版 D-2 留白,本版补齐)— 【亲验,全绿属实】
Gitea commit status API 逐仓亲查(匿名 GET `…/api/v1/repos/zhaoyuxi/<repo>/commits/<HEAD>/status`):
| 仓库 | HEAD | state | context | 耗时 |
| --- | --- | --- | --- | --- |
| patbond-api | `0d81c38` | **success** | CI / backend-test (push) | 3m18s |
| patbond-flutter | `3f8388e` | **success** | CI / flutter-gates (push) | 50s |
| patbond-doc | `5537f92` | **success** | CI / docs-build (push) | 12m21s |
08 号报告「CI 状态经 commit status API 逐仓核实」**属实**D-2 关闭。
**附带发现(低,需用户确认意图)**:上述 API 从本机**匿名(无 token)即可读取**,仓库信息(含 owner 邮箱)对未认证请求可见。若 Gitea 实例意图私有,建议核对实例的匿名访问/仓库可见性设置。本报告不含任何凭据。
---
## 3. 同伴报告高影响声称抽查(3 证实 + 2 证伪/纠正)
### 3.1 「AnalyticsService 生产未接线」— 【亲验**证实**,且比声称更严重】
调用链逐行核对:`lib/main.dart``runApp(const App())``lib/app/app.dart` 全文**零** analytics 引用,`_buildRepository()`app.dart:38-51)构造 `ApiAuthRepository` 时不传可选参数 `_analytics`auth_repository.dart:38、:45 `AnalyticsService? _analytics`)→ 生产路径 `_analytics` 恒为 null,登录/注册/退出三个已挂接点(auth_repository.dart:62-119)的 `_analytics?.` 调用**全部空转**。全仓 grep:`AnalyticsService(` 仅在其自身定义与测试中出现。
**推论(此前无人点破)**:生产 App 自 M1 上线以来**从未发出过任何事件**。06 号报告的 M2 指标体系、对账 SQL、「M1 存量指标不回退」护栏全部建立在有数据流入的假设上——接线不修,M2 全部指标为零数据。D-1 因此升级:修接线必然要消费 `POST /api/v1/events`,契约缺口必须先补。
### 3.2 「bootstrap SQL 1156~1166 行四条跨 schema 外键」— 【亲验**证实**,01 号报告准确】
`docs/database/patbond_postgresql.sql` 实测:`ALTER TABLE pet_health.pet_vaccinations`:1156)加 `fk_vaccinations_provider`(:1157)/`fk_vaccinations_booking`(:1159)`ALTER TABLE pet_health.health_events`:1162)加 `fk_health_events_provider`(:1163)/`fk_health_events_booking`(:1165),四条均 REFERENCES `marketplace.providers/bookings`。01 号报告 T2-01 的行号与「V3 必须剥离」裁剪项**完全属实**。
**勘误(02 号报告 :32 被证伪)**02 号称「目标模型中 `pet_vaccinations.provider_id/booking_id``health_events.provider_id/booking_id` 本就未设 FK(预留列)」——**与 SQL 原文不符**,外键就在上述行号。两报告矛盾时以 01 号为准;照抄 bootstrap SQL 的 V3 在无 marketplace schema 的干净库上会直接失败(01 号 R3 风险为真)。
### 3.3 「analytics_service.dart 三处偏差」— 【亲验**证实**,另发现 06 号一处基线失实】
06 号报告 §0 三处偏差逐行核对,行号全部命中:
1. sessionId 每事件独立生成:analytics_service.dart:55 `'sessionId': const Uuid().v4()`
2. eventId 用 UUID v4 非 v7:50 `'eventId': const Uuid().v4()`
3. appVersion/osVersion 硬编码::57 `'1.0.0+1' // TODO`、:58-61 `'android-14'/'ios-17' // TODO`
**勘误(06 号基线表另一行被证伪)**06 号称「Flutter 队列 | shared_preferences 持久化,上限 500 条」——这是照抄了文件头**过期注释**(:6-9)。实际实现:**内存队列、阈值 20 条**(:18-19 注释自认「持久化队列留 M1」、:25 `_pendingEvents`),上传失败**整批丢弃**(:80-84)。App 一杀进程未满 20 条的事件全部丢失。06 号「事件丢失率 < 5%」的护栏在此实现下无保障——不过在 §3.1(根本没接线)面前,这暂时只是第二层问题。
---
## 4. 「声称 vs 实际」差异表(复核版)
| # | 声称 | 实测 | 严重度 |
| --- | --- | --- | --- |
| D-1 | OpenAPI 契约正式化 | 缺 `POST /api/v1/events`(grep 0 命中);因 M2 必须修埋点接线并消费该端点,从「中」**上调为高优先** | **中→高** |
| D-2 | CI 全绿 | 本人 API 亲查三仓 HEAD 全 success**关闭** | 已关闭 |
| D-4(新) | 三仓干净(初版 RC-1、08 号) | patbond-doc 现有 mkdocs.yml 修改 + 8 份报告未跟踪,全部未提交未推送 | **中**(流程风险复发苗头) |
| D-5(新) | 埋点「已挂 3/5 挂接点」(19/06 号語境暗示在采数) | 生产装配未接线,事件流恒为零;挂接点代码存在但空转 | **高**M2 指标体系的前提为假) |
| D-6(新) | 06 号:队列 shared_preferences 持久化 500 条 | 内存队列 20 条、失败丢弃(代码 :18/:25/:80-84 | 低(被 D-5 覆盖,接线后需修) |
| D-7(新) | 02 号 :32:目标模型未设 provider/booking FK | bootstrap SQL :1156-1166 四条跨 schema FK 确凿存在,01 号正确 | 中(若按 02 号理解仍会做对,但依据是错的;V3 评审须以 SQL 原文为准) |
| D-3 | (环境)本地 mkdocs pip vs CI apt | 维持初版判断 | 低 |
| — | E2E 7/7、真机契约偏差 0 | **UNVERIFIED**(本地不可复现,无一手证据) | 待 M2 早期回归裁决 |
初版「7 项核查 6 项属实、1 项部分属实」的口径修正为:**核心测试/CI/环境基线全部亲验属实;但初版自身含一处计数错误(RC-2),且其「三仓干净」结论在当前时点已失效**。
## 5. 放行条件清单(CONDITIONAL PASS 的条件)
1. **提交并推送 patbond-doc 当前未提交内容**8 份开工报告 + mkdocs.yml 导航),第一波内完成,CI docs-build 须绿。不允许带着未提交文档开工——这是第一迭代原教训。
2. **契约冻结前把 `POST /api/v1/events` 补入 openapi.yaml**(或书面拍板「内部契约不入 OpenAPI」并留痕)。M2 走契约先行,基线契约不能自带游离端点。
3. **M2 第一波跑一轮 E2E 回归**,把 UNVERIFIED 的联调通道状态变成一手证据;通道已腐化则立刻修,不许拖到中后期。
4. **埋点生产接线立为 M2 显式工单**App 装配传入 AnalyticsService + 06 号三偏差修复 + 队列持久化按 06 §3 验收),并在工单中注明「当前生产事件流为零」这一事实,防止指标基线被误读。
5. **V3 迁移评审以 bootstrap SQL 原文为准**:1156-1166 四条 FK 必须剥离,01 号 T2-01 裁剪项照办;02 号 :32 的表述作废),验收含全新 Testcontainers 库 V1→V3 全量迁移一次成功。
条件 1、2 在第一波内完成即可,不阻塞今日开工排期;条件 3~5 已有对应工单/裁剪项,本清单是把它们钉死为放行前提。
## 6. 合规确认
- 三仓生产代码零写入;未 commit、未 push、未改 mkdocs.yml(其现有修改为前序波次所留,本人未触碰)。本文件为 patbond-doc 中未跟踪的报告文件,按授权原地更新,文件名未改。
- Gitea 取证为匿名只读 GET,未使用亦未记录任何凭据;报告不含敏感信息。
- 测试/构建日志留存于会话 scratchpadmvn-test-recheck.log、flutter-test-recheck.log),未混入仓库。
---
**复核人**TestingRealityChecker · 证据分档:【亲验】/【UNVERIFIED】/【勘误】 · 再评估时点:放行条件 1~3 完成后
@@ -0,0 +1,274 @@
# 05 · 第二迭代 宠物健康档案 UI 设计规范
> 作者:UI Designer
> 日期:2026-09-07
> 迭代:Iteration 2「M2 宠物健康档案」
> 素材来源:`AI宠物_iOS_UI设计稿.html`(品牌正典,ADR-005)、`patbond-flutter/lib/core/theme/app_theme.dart`(已落地 token)、`lib/widgets/common.dart` 与 `lib/core/widgets/`(既有组件)、`lib/features/pets/pets_page.dart`(档案页现状)、第一迭代 04/12 号 UI 报告(规范基线)
> 性质:开工前设计规范;只定规格,不改代码
---
## 0. 正典设计语言提炼(宠物档案相关)
正典 HTML「宠物成长档案」画框已给出的语言,本规范全部延续:
| 正典元素 | 描述 | 对应 Flutter 现状 |
| --- | --- | --- |
| `patbond-header` | 居中头像(76,3px 白描边 + 轻投影)+ 名字(Baloo 2 17+ 元信息(11 muted | `pets_page.dart` 头部已实现(头像 104 |
| `stat-row` / `stat-card` | 三等分白卡:大数值(coral-dark 加粗)+ 小标签(muted | `_StatCard` 已实现 |
| `alert-card` | sage 底 AI 健康提醒卡(dot + 文字) | 健康提醒卡已实现(successSurface 族标准用法,12 报告 §3 认可) |
| `timeline-item` | 30px peach 圆底 emoji 图标 + 标题(12/w600+ 日期(10 muted),**无卡片包裹** | `_TimelineTile` 实现为卡片式(CircleAvatar + SectionCard),比正典重 |
| `section-title` | 分区标题 | `titleLarge` 18/w800 |
| `chip` / `chip.active` | 胶囊筛选:白底 border 描边 muted 字;选中态 coral 实底白字 | 未实现共享组件 |
| `stories` 头像环 | brandGradient 2px 渐变环 + 白描边头像 | 首页已有 |
正典**未覆盖**(详见 §6 待拍板清单):宠物列表页(多宠物)、完整时间线与类型筛选(正典只有「最近记录」3 条)、记录详情页、新增/编辑记录表单、体重/驱虫/就医的记录类型视觉。这些页面为本规范新增提案。
---
## 1. 页面族总览
```text
档案 Tab
└─ P1 宠物列表(多宠物入口;单宠物时直进 P2,见 §6 D1)
└─ P2 健康档案页(宠物头 + 数据卡 + 提醒 + 时间线 + 筛选 + 新增入口)
├─ P3 记录详情(push 页)
│ └─ P4 编辑记录(modal bottom sheet
└─ P4 新增记录(modal bottom sheetFAB 触发)
```
通用排版 token(延续一迭代规范与现有实现,不新造):
- 页面内边距:`EdgeInsets.fromLTRB(16, 16, 16, 30)`(与现有五个 Tab 页一致)
- 间距刻度:4 / 8 / 12 / 16 / 24 / 32;卡片间距 1012,分区间距 22–24
- 圆角:卡片 `AppRadius.xl`(24Card 主题默认)、输入框 `lg`(18)、sheet 内 CTA `md`(16)、徽章/chip `pill`
- 字级:分区标题 `titleLarge` 18/w800;卡内标题 `titleMedium` 15/w700;正文 `bodyMedium` 14;次级 12**色用 `inkSoft`,不用 `muted`,见 §5 DEBT-2**
- Bottom sheet 统一沿用 `EditPetSheet` 既有骨架:`_SheetHandle`44×5 `border` 色胶囊)+ 标题行(`titleLarge` + 右侧 close)+ 内容 + 全宽提交按钮;`padding EdgeInsets.fromLTRB(20, 10, 20, viewInsets.bottom + 20)`
---
## 2. 记录类型体系(图标 + 色彩映射)
M2 记录类型五种(「其他」为扩展兜底)。每种类型 = 图标 + 一族三色:**dot 底**(基础色 8%,`withAlpha(20)`,与 TagPill/InlineErrorBanner 既有做法一致)、**图标色**(非文字对比 ≥3:1WCAG 1.4.11)、**文字色**(≥4.5:1WCAG AA)。
| 类型 | 图标(Material | dot 底(8% tint/白底合成值) | 图标色 | 图标对比 | 文字/标签色 | 文字对比(于 dot 底) |
| --- | --- | --- | --- | --- | --- | --- |
| 体重 | `monitor_weight_outlined` | `primary` 8% → `#FFF4F1` | `primaryStrong` | 4.16:1 | `primaryDark` | 8.74:1 |
| 疫苗 | `vaccines_outlined` | `success` 8% → `#F5F8F6` | `successInk` | 7.39:1 | `successInk` | 7.39:1 |
| 驱虫 | `pest_control` | `accent` 8% → `#FFF9F1` | `accentDark` | 7.07:1 | `accentDark` | 7.07:1 |
| 就医 | `medical_services_outlined` | `error` 8% → `#FBEFEE` | `error` | 4.44:1 | `errorDark`(新 token 提案) | 5.78:1 |
| 其他 | `sticky_note_2_outlined` | `muted` 8% → `#F7F6F4` | `inkSoft`(新 token 提案) | 6.10:1 | `inkSoft` | 6.10:1 |
映射依据:体重是核心品牌数据 → primary 族(正典 stat-card 数值即 coral-dark);疫苗延续现有实现的 success 族(疫苗进度环、健康提醒已用 sage);驱虫用 accent 族(提醒/预防语义,正典徽章族);就医用 error 族(医疗警示语义)。
**新增语义 token 提案(2 个,待拍板):**
| Token | 值 | 派生逻辑 | 用途 |
| --- | --- | --- | --- |
| `errorDark` | `#B02C25` | `error #D0342C` 加深(与 primary→primaryStrong 同构) | error 淡底上的文字(`error` 本身在自家 8% 底上仅 4.44:1,贴线不过);就医类型文字 |
| `inkSoft` | `#6B5A4A` | **直接取自正典**feed-caption 文字色,非新造) | 承载信息的次级文字(日期、元数据);白底 6.59:1、canvas 底 6.21:1、surfaceTint 底 5.58:1 全达标 |
注:疫苗/驱虫/其他三型图标直接用深变体(`success #7FA88A` 在白底仅 2.67:1,无中间档可用);体重/就医图标可用中强度变体保留彩度,均 ≥3:1。所有类型图标**必须与文字标签成对出现**,不得单独用色彩区分类型(色盲可辨性)。
---
## 3. 新组件规格(4 个)
### 3.1 `PetAvatar` 宠物头像(`lib/core/widgets/pet_avatar.dart`
统一现有两处各写一遍的头像代码(`pets_page.dart` 档案头 104、EditPetSheet 96)。
- **构成**`RemoteImage` 圆形裁切(复用其 loading `surfaceTint` 块 / 失败 `Icons.pets` muted 兜底)+ 3px `surface` 白描边 + 投影 `rgba(0,0,0,0.08) 0 4 10`(正典 `.patbond-avatar` 规格)+ 可选右下编辑徽标。
- **尺寸档**`xl` 96(档案页头部,收敛现有 104 → 96,与 EditPetSheet 一致)、`lg` 64(宠物列表卡)、`md` 44(头部宠物切换器,恰为最小触控目标)、`sm` 32(记录详情等行内)。徽标:xl/lg 32 圆(`primaryStrong` 底 + 白 `edit` 图标 15,白/`primaryStrong` 4.49:1;现实现用 `primary` 底,白图标 2.75:1 不达非文字 3:1,本规范修订为 `primaryStrong`),md/sm 不带徽标。
- **可选渐变环**`ring: true` 时外圈 2px `brandGradient`(正典 story 环),仅用于「当前选中宠物」指示,纯装饰。
- **状态**:默认;可点击时 `InkWell` 圆形 ripple;禁用 60% 不透明度(对齐 `AppTextField` 禁用惯例);加载/失败由 `RemoteImage` 兜底。
### 3.2 `RecordTypeDot` 记录类型圆标(`lib/core/widgets/record_type_dot.dart`
§2 映射表的唯一渲染出口——类型↔色彩映射内置于组件,调用方只传类型枚举,杜绝散落硬编码。
- **尺寸档**`md` 40(时间线,正典 30 于 320 画框的真机放大)、`lg` 56(记录详情页头)、`sm` 24(表单类型选择器内)。图标尺寸 = dot 的 50%。
- **规格**:正圆,底色/图标色按 §2 表;无自身点击态(点击归属父容器);无禁用态。
- 同文件导出类型→文字色/标签文案的映射常量,供 TagPill、详情页复用。
### 3.3 `HealthTimelineTile` 时间线条目(`lib/core/widgets/health_timeline_tile.dart`
`pets_page.dart` 私有 `_TimelineTile` 升级为共享组件(保留其卡片式形态——比正典裸排版更适合可点击的密集列表,判定为可接受偏离)。
- **布局**`Card`(主题默认:白底、`border` 1px、圆角 24、零 elevation)内 `Row`padding 14`RecordTypeDot(md)` → 12 → 内容列(标题 `titleMedium` 15/w700 `ink`;第二行 12 `inkSoft`:日期 + " · " + 摘要,如「2026-06-12 · 瑞派宠物医院」)→ 尾部插槽:数值型记录显示大数值(15/w800,类型文字色,如体重「5.2kg」`primaryDark`),事件型记录显示 `TagPill`DEBT-1 修复后形态,§5)。
- **左轨连线**:相邻条目 dot 间 2px `border` 色竖线(画在卡外左轨)。实现代价高时可省略——正典 timeline 本无连线,省略不算偏离。
- **状态**:默认;按下 `InkWell` ripple(圆角随卡 24);整卡可点进 P3;无禁用态。整卡高约 68,触控达标。
### 3.4 `EmptyStateIllustration` 空态插画区(`lib/core/widgets/empty_state_illustration.dart`
现有 `EmptyState`42 图标 + 一行 bodySmall)不足以承载引导动作,新组件向上兼容。
- **布局**(垂直居中,上下留白 48):112 圆形插画区(`surfaceTint` 底 + 56 图标 `primary`——大面积装饰用法,primary 合法)→ 16 → 标题 `titleMedium` `ink` → 8 → 说明 12 `inkSoft`(≤2 行居中)→ 24 → 可选 CTA(`FilledButton`,主题默认 52 高,非全宽自适应内容 + 水平 padding 24)。
- **插画**v1 用 Material 图标(无宠物态 `Icons.pets`;无记录态 `Icons.event_note_outlined`);正式插画素材待品牌侧供给后原位替换,尺寸档不变。
- **状态**:静态组件,仅 CTA 有按下/禁用(随按钮主题)。
---
## 4. 页面规范
### 4.1 P1 宠物列表 【设计稿未覆盖,本规范为新增提案,待拍板】
档案 Tab 落地页(多宠物时)。页面 padding 通用值。
```text
我的宠物 titleLarge,与「添加」TextButton.icon 同行
↓ 12
┌──────────────────────────────┐
│ [PetAvatar lg64] 豆豆 │ 宠物卡:Card 主题默认,padding 14
│ 柴犬 · 2岁 · 5.2kg │ 名字 titleMedium;元信息 12 inkSoftchevron muted
└──────────────────────────────┘
↓ 10(卡间距)
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
+ 添加宠物 虚线卡:border 色 1.5px dashedradius 24
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ 高 64,文字 14/w600 primaryStrong(白底 4.49:1
```
- 宠物卡状态:默认 / 按下 ripple → push P2;当前选中宠物可加 `PetAvatar ring`
- **空态**0 宠物):`EmptyStateIllustration`——`Icons.pets`、「还没有宠物档案」、「添加毛孩子,开始记录 TA 的健康点滴」、CTA「添加宠物」→ 复用 `EditPetSheet`
- 既有组件:Card、TextButton;新组件:PetAvatar、EmptyStateIllustration。
### 4.2 P2 健康档案页(正典「宠物成长档案」画框的扩展)
结构自上而下(既有实现骨架保留,标注改动点):
| 区块 | 规格 | 出处 |
| --- | --- | --- |
| 宠物头部 | `PetAvatar(xl 96, 编辑徽标)` + 8 + 名字 `headlineSmall` + 4 + 元信息 12 `inkSoft`(现为 muted,随 DEBT-2 修订);多宠物时名字旁加切换箭头,点开 `md 44` 头像横排选择 sheet | 正典 patbond-header;切换器为新增提案 |
| 数据卡行 | 三张 `_StatCard`(升共享):体重 / 疫苗进度 / **本月记录数**。图标色按 §2 类型色(体重卡图标 `primaryStrong`,修订现值 `primary`);数值 15/w800 `ink`;标签 12 `inkSoft`。点击体重卡 → 时间线过滤体重;点疫苗卡 → 疫苗管理 sheet(既有) | 正典 stat-row;**出入**:正典第三卡为「本月花费 ¥328」,花费域不在 M2 范围,改为「本月记录」,待拍板 |
| AI 健康提醒 | 现有 successSurface 提醒卡原样保留 | 正典 alert-card |
| 分区标题 | 「健康时间线」`titleLarge` | 正典 section-title(原文案「最近记录」) |
| 类型筛选 chips | 见下 | 正典 chip 形态 + 无障碍修订 |
| 时间线 | `HealthTimelineTile` 列表,按月分组,组头 12/w700 `inkSoft`(「2026 年 9 月」)上 16 下 8 | 正典仅 3 条「最近记录」,完整时间线为新增提案 |
| 新增入口 | FAB56 圆,`primaryStrong` 底 + 白 `add` 图标(4.49:1),右下距边 16、距 TabBar 上沿 16 → 打开 P4 sheet | 【设计稿未覆盖,新增提案,待拍板】 |
**筛选 chip 规格**(全部 / 体重 / 疫苗 / 驱虫 / 就医):高 36(上下各留 4 达 44 触控),水平 padding 14,圆角 `pill`,文字 13/w600,间距 8,横向滚动。未选中:`surface` 底 + `border` 1px + `inkSoft` 字(6.59:1)。选中:`surfaceTint` 底 + `primaryDark` 字/w7007.98:1)。
**偏离正典声明**:正典 `chip.active` 为 coral 实底白字(2.75:1,不达 AA),不采纳;选中态改为 surfaceTint + 深字,与 NavigationBar 既有选中指示(surfaceTint indicator)同语言。
**状态**:加载 = 头部骨架(surfaceTint 块)+ 居中 `CircularProgressIndicator`;时间线空态 = `EmptyStateIllustration``event_note_outlined`、「还没有健康记录」、CTA「记录第一条」;筛选后空态文案「暂无某某记录」且无 CTA);加载失败 = `InlineErrorBanner` + 重试按钮,瞬态错误走 SnackBar(一迭代三层错误模型沿用)。
### 4.3 P3 记录详情 【设计稿未覆盖,本规范为新增提案,待拍板】
push 页,透明 AppBar 仅返回箭头(`ink` 色,沿用注册页惯例),右上 `edit_outlined` IconButton44 触控)→ P4 编辑态。
```text
[RecordTypeDot lg56] ← 左对齐,与标题同行或其上
狂犬疫苗接种 headlineSmall 22 ink
[疫苗] 2026-06-12 TagPill(修复后) + 日期 14 inkSoft,间距 8
↓ 24
┌ SectionCard(padding 18) ───────┐
│ 字段名 12 inkSoft │ 键值对列表,行距 14;
│ 字段值 bodyMedium 14 ink │ 体重类数值行:值 20/w800 primaryDark
│ ────── 分隔线 border 1px ────── │
│ … │
└────────────────────────────────┘
↓ 16
备注:SectionCard 内 bodyMedium ink、行高 1.5(无备注则整卡不渲染)
照片:3 列网格,间距 8RemoteImage 1:1 圆角 sm12(无照片不渲染)
↓ 24
删除记录 TextButton 全宽居中,error 色字(白底 4.99:1
```
删除走 `AlertDialog` 确认(「删除后不可恢复」,确认钮 `FilledButton` error 底白字 4.99:1,取消 `TextButton`)。删除属破坏性动作,必须确认。
### 4.4 P4 新增/编辑记录表单 【设计稿未覆盖,本规范为新增提案,待拍板;骨架沿用既有 EditPetSheet 模式】
`showModalBottomSheet(isScrollControlled: true, useSafeArea: true)`,§1 通用 sheet 骨架。标题「新增记录」/「编辑记录」。
- **类型选择器**(仅新增态;编辑态锁定,显示为静态 dot+标签):五个垂直单元(`RecordTypeDot sm24` 上、11/w600 标签下)横排等分;选中单元 `surfaceTint` 底圆角 sm12 + `primaryDark` 标签,未选中标签 `inkSoft`;单元 ≥44×52 触控。
- **动态字段**(全部走既有 `inputDecorationTheme`;日期用 EditPetSheet 的 `ListTile` + `showDatePicker` 模式;标 * 为必填):
| 类型 | 字段 |
| --- | --- |
| 体重 | 体重 kg*(数字键盘,>0 且 ≤200 校验)、日期*(默认今天) |
| 疫苗 | 疫苗名称*、接种日期*、医院/机构、下次接种提醒日期 |
| 驱虫 | 体内/体外/体内外*`SegmentedButton`,主题派生色)、日期*、药品名称 |
| 就医 | 主题/症状*、就诊日期*、医院、诊断结果(多行)、花费 ¥(数字,选填) |
| 其他 | 标题*、日期* |
| 通用尾部 | 备注(多行 3 行高)、照片(64 方格「+」添加,border 虚线,最多 9 张,`RemoteImage` 预览 + 右上删除角标) |
- **校验与错误**:失焦 + 提交双校验,字段错误走 `errorText`(一迭代惯例:`onChanged` 即清除);不可归属错误 → 提交按钮上方 `InlineErrorBanner`;网络瞬态 → SnackBar+重试。文案示例:「请输入体重」「体重需在 0–200kg 之间」「请选择日期」。
- **提交**`PrimaryButton`(isLoading 转圈锁尺寸)「保存记录」;成功 pop 并 SnackBar「已保存」,时间线原位刷新。
- 字段间距 12EditPetSheet 现值),分组间距 20。
---
## 5. 色彩无障碍自查(WCAG AA
计算方法:WCAG 2.x 相对亮度公式,8% 淡底按 `withAlpha(20)`(=7.84%)与承载底合成后计算。正文阈值 4.5:1,大字(≥18.7px 加粗 / 24px3:1,非文字元素 3:1。
### 5.1 本规范用到的全部文字组合
| 组合 | 对比度 | 判定 |
| --- | --- | --- |
| `ink` / `surface``canvas``surfaceTint` | 13.50 / 12.71 / 11.42 | 达标 |
| `inkSoft #6B5A4A` / `surface``canvas``surfaceTint` | 6.59 / 6.21 / 5.58 | 达标(新 token 提案) |
| `primaryDark` / `surface``canvas``surfaceTint`、primary 8% 底 | 9.43 / 8.88 / 7.98 / 8.74 | 达标 |
| `primaryStrong` / `surface`(链接、添加宠物字);白字 / `primaryStrong`FAB、按钮) | 4.49 / 4.49 | 达标(一迭代已裁决按 ≈4.5 采纳) |
| `successInk` / success 8% 底、`successSurface` | 7.39 / 6.79 | 达标 |
| `accentDark` / accent 8% 底 | 7.07 | 达标 |
| `errorDark #B02C25` / error 8% 底(就医标签) | 5.78 | 达标(新 token 提案) |
| `error` / `surface`(删除按钮);白字 / `error`(确认删除钮) | 4.99 / 4.99 | 达标 |
| 非文字:各类型图标于 dot 底(§2 表) | 4.167.39 | 均 ≥3,达标 |
### 5.2 不采纳的正典/现状组合(本规范修订点)
| 组合 | 对比度 | 处置 |
| --- | --- | --- |
| 正典 chip.active:白字 / `primary` | 2.75 | 选中 chip 改 `surfaceTint` 底 + `primaryDark` 字(§4.2 |
| 现档案页头像编辑徽标:白图标 / `primary` 底 | 2.75(非文字需 ≥3) | `PetAvatar` 徽标底改 `primaryStrong`(§3.1 |
| `muted` / `surface``canvas` | 3.36 / 3.16 | 见 DEBT-2 |
| TagPill 现状:`primary``success``accent` 文字于自身 8% 底 | 2.55 / 2.50 / 1.67 | 见 DEBT-1 |
### 5.3 DEBT-1TagPill)偿还方案 —— **建议:借 M2 一并偿还**
理由:健康档案时间线每条记录带一枚类型标签,TagPill 用量将从当前 5 处增至列表级高频;带着 2.5:1 的标签上新页面等于把债务翻倍,且 §2 的类型文字色映射本身就是 TagPill 需要的深变体映射,修复与新功能是同一套色。
方案(照一迭代 12 报告 §2.3 既定方向细化):
1. `TagPill` 增加可选 `inkColor` 参数:底色维持 `color.withAlpha(20)` 不变,文字改用 `inkColor`
2. 内置默认映射(`inkColor` 缺省时按 `color` 查表):`primary → primaryDark`8.74:1)、`success → successInk`7.39:1)、`accent → accentDark`7.07:1)、`error → errorDark`5.78:1)、未命中 → `ink`(≥12:1 兜底)。
3. 字号 11/w700 维持不变——修色后 11px 小字达标(AA 对小字与正文同阈值,上表均 ≥5.7)。
4. 回归范围:现有 5 处调用(服务页「认证服务」、档案时间线状态标签等)零参数变更、仅视觉变深;`flutter test` 全量回归。工作量一行映射表 + 一个参数,建议与 `RecordTypeDot` 同一工单。
### 5.4 DEBT-2(新发现,提案):`muted` 作信息文字不达 AA
`muted #9C8977` 在白底 3.36:1、canvas 底 3.16:1,低于正文 4.5:1。这是随正典色板继承的既有债(一迭代自查只覆盖了 primaryStrong/ink/error 三组,未查 muted),全 app bodySmall 均受影响,**不阻塞 M2、不在 M2 全局翻修**。M2 范围内的处置:
- 健康档案页面族中**承载信息**的次级文字(记录日期、宠物元信息、字段名、月份组头)一律用 `inkSoft #6B5A4A`(正典既有色,6.59:1);`muted` 仅限占位符、禁用态、纯装饰。
- 全局层面(bodySmall 默认色是否切 `inkSoft`)另立议题,交 M2 之后拍板——影响面是全部五个 Tab,需要整体视觉复核。
---
## 6. 与正典出入 / 待拍板清单
| # | 事项 | 性质 |
| --- | --- | --- |
| D1 | P1 宠物列表页整页(正典档案 Tab 直落单宠物页)。附决策点:单宠物时是否跳过列表直进 P2(本规范建议:跳过,P2 头部留切换器) | 设计稿未覆盖,新增提案 |
| D2 | 完整健康时间线 + 类型筛选 chips(正典仅「最近记录」3 条) | 设计稿未覆盖,新增提案 |
| D3 | P3 记录详情页整页 | 设计稿未覆盖,新增提案 |
| D4 | P4 新增/编辑表单(骨架沿用既有 EditPetSheet 先例,仅字段为新) | 设计稿未覆盖,新增提案 |
| D5 | FAB 新增入口(正典无浮动按钮语言;备选:时间线分区标题右侧「+记录」TextButton) | 设计稿未覆盖,新增提案 |
| D6 | stat-row 第三卡「本月花费」→「本月记录」(花费域不在 M2) | 与正典有出入 |
| D7 | 选中 chip 弃用正典 coral 实底白字(2.75:1),改 surfaceTint + primaryDark | 无障碍修订偏离 |
| D8 | 新 token`errorDark #B02C25``inkSoft #6B5A4A`(后者取自正典既有色值) | token 提案 |
| D9 | DEBT-1 随 M2 偿还(§5.3);DEBT-2 记账、M2 内局部规避(§5.4) | 债务处置提案 |
| D10 | 时间线条目维持卡片式(偏离正典裸排版,沿用现实现形态) | 可接受偏离,随 D2 一并确认 |
---
## 7. 交付验收对照(供开发/QA)
- [ ] 4 个新组件(PetAvatar / RecordTypeDot / HealthTimelineTile / EmptyStateIllustration)落位 `lib/core/widgets/`,类型色彩映射只存在于 `RecordTypeDot` 一处。
- [ ] 4 个页面均具备 loading / empty / error / retry 态;错误三层模型(字段 errorText / InlineErrorBanner / SnackBar)与一迭代一致。
- [ ] 本规范全部文字组合按 §5.1 达 AA;类型仅靠「图标+文字」双通道区分,不单靠颜色。
- [ ] TagPill 修复合入(若 D9 拍板通过),现有 5 处调用回归无布局变化。
- [ ] 删除记录有确认对话框;所有触控目标 ≥44×44。
- [ ] `AuthScaffold` 内禁用 Spacer、按钮 `minimumSize Size(64,52)` 等一迭代既定约束不回退(本页面族不涉及 AuthScaffold,sheet/页面沿用各自既有骨架)。
---
**UI Designer** · 2026-09-07
@@ -0,0 +1,524 @@
# 第二迭代埋点与实验规划(宠物健康档案)
> 角色:Experiment Tracker(本版为角色复核定稿;初版由通用 agent 代拟,已整体接管)
> 日期:2026-09-07
> 前序:iteration-1 `05-experiment-tracking-plan.md`(事件与指标规划)、`13-tracking-implementation-spec.md`(工程规范与字典 v1)、`19-analytics-implementation-report.md`M0 简化版落地实况)
> 依据:`development-plan.md` 第 7 节 M2、第 9 节「可观测性与产品验证」;`patbond-api` `EventDictionary.java` 现行白名单;`patbond-flutter` `lib/analytics/analytics_service.dart` 现状;本迭代 `01-pm-task-breakdown.md`M2 范围与验收)
> 范围:M2 健康档案纵切(宠物、体重、疫苗、健康事件、提醒);社区、AI 创作、本地服务不在本轮定义
> 性质:纯规划文档,供 M2 开发工单直接引用;不含任何代码改动
**本版相对初版的复核结论(速览)**
1. 初版的事件字典 v2 增量(10 事件)、护栏指标、对账 SQL、基础设施评估经复核**基本成立,予以保留**;漏斗闭环复核见 §1.6,发现并修订一处实质缺口(pageName 枚举缺 `pet_form`)。
2. 北极星初版只给了方向没给可操作口径——本版**落定候选 A「7 日回访记录率」的完整定义式**(分母、去重、窗口边界、成熟期、SQL),见 §2.1。
3. **新增 4 条可证伪产品假设 H1–H4**(初版完全缺失),每条带判定指标、阈值、数据源、观察窗口与证伪后行动,见 §3——这是实验规划区别于纯埋点规划的核心。
4. 「M2 不启动 A/B」的判断成立,但初版只说「前置未绿」不给路线——本版给出 8 项前置条件 × 预计达成迭代,结论:**M3 末可全绿,M4 启动首个实验**,见 §4。
5. 客户端三处偏差声称**已由本角色重新实读代码逐一实锤**(§0);废弃 `health_record_action` 的立场:**同意直接移除**,并补充实验视角理由(§1.2)。
---
## 0. 基线现状(开工前核对)
| 项 | 现状 | 出处 |
| --- | --- | --- |
| 后端接收端 | `POST /api/v1/events` 已上线:批量 1–50 条、202 逐条结果、eventId 幂等、白名单剥离、红线拒绝、匿名可报 | 报告 19 §1.1 |
| 后端字典 | v1 的 11 个 `auth_*` 事件 + 工单增补 `page_viewed(pageName, referrer)``health_record_action(recordType, actionType)` | `EventDictionary.java` |
| 存储 | `platform.product_events`Flyway V2v1 不分区,触发分区阈值约 5,000 万行) | 报告 13 §2 |
| Flutter 采集 | `AnalyticsService` 已挂 3/5 挂接点(登录/注册/退出);`page_viewed``health_record_action` 仅 TODO 注释 | 报告 19 §1.2 |
| Flutter 队列 | shared_preferences 持久化,上限 500 条 | `analytics_service.dart` |
### 0.1 客户端三处偏差(本角色实读 `analytics_service.dart` 复核,全部实锤)
| # | 偏差 | 证据(行号) | 对实验数据的影响 |
| --- | --- | --- | --- |
| 1 | `sessionId` 每事件独立生成 | 第 55 行 `'sessionId': const Uuid().v4(), // Simplified: unique per event (M0)` | 会话维度整体不可用:§6.3 巡检、护栏 5 的代偿口径、page_viewed 覆盖率 sanity 全部依赖它 |
| 2 | `eventId` 为 UUID v4 而非规范要求的 v7 | 第 50 行 `'eventId': const Uuid().v4()` | 去重不受影响;随机主键丧失插入时间局部性,量级上来后 B-tree 写放大 |
| 3 | `appVersion`/`osVersion` 硬编码 | 第 57 行 `'1.0.0+1'`;第 5961 行 `'android-14'`/`'ios-17'`(均留 TODO) | 版本维度全体失真,M2 起按版本切片看回归不可行 |
结论:接收链路可信、可直接承载 M2 新事件;三处偏差**须在 M2 第一波修复**(§5、§7.2),否则本迭代新指标的会话与版本维度都是坏数据。§3 的假设判定与 §2.1 北极星均已刻意设计为**不依赖 sessionId**(只用 userId + server_ts),即便修复延迟,核心读数不受污染——但漏斗 sanity 与护栏会瞎。
---
## 1. 事件字典 v2 增量(health_record 域)
### 1.1 沿用 v1 的设计原则(不复述,仅列约束)
命名 `<域>_<动作>_<结果>` snake_case`eventVersion` 起始 1、变更递增禁止原地改语义;客户端采集、`serverTs` 服务端补写为统计权威时间;`eventId` UUIDv7 幂等;属性 camelCase;公共属性(报告 13 §4.0 十项)全体必带。M2 新增两个域前缀:**`pet`**(宠物实体)与 **`health_record`**(档案记录)。
### 1.2 `health_record_action` 保留位的处置:废弃并直接移除(本角色立场:同意)
M0 工单在档案功能设计之前,往后端字典预置了通用事件 `health_record_action(recordType, actionType)`。v2 决定**不启用该保留位,以细分事件取代**:
1. v1 惯例把结果编码进事件名(`_succeeded`/`_failed`),使每个事件有独立 props 白名单与独立失败枚举;`actionType` 把 4 种动作塞进一个事件,白名单只能取并集,失败语义无处安放。
2. 漏斗指标(§2.2)需要 `started → succeeded` 配对事件,通用事件表达不了。
3. **实验视角补充理由(本角色)**:假设验证要求「一个指标定义式只引用语义单一的事件」。若 H1(记录类型分布)与漏斗完成率共用一个 `health_record_action`,则任何一次 `actionType` 枚举扩充都会同时污染两套指标口径的分母——细分事件把这种耦合从源头切断。§3 全部 4 条假设都以细分事件为数据源,保留位对假设验证零贡献。
4. **废弃是零成本的**:本角色 grep 全库核实,`patbond-flutter/lib` 下对该事件名 **0 处引用**(仅后端白名单一行 + 注释),不存在兼容负担。
处置:后端工单从 `EventDictionary` 白名单**直接移除**该条目(连同 `actionType``recordType` 作为属性名由 §1.4 各细分事件继承);Flutter 侧 TODO 注释指向的挂接位置改挂 §1.4 细分事件。
同场收编:`page_viewed(pageName, referrer)` 同为工单增补、未进字典正稿,v2 将其**转正**(定义见 §5.2,pageName 必须是枚举,禁止自由路由字符串)。
### 1.3 隐私红线增量(在 v1 六条红线之上追加,针对档案内容)
埋点只记录**行为**,不记录**内容**——内容分析一律走服务端事实表(M2 验收「体重、疫苗进度……从事实表聚合」本来就要求事实表可查)。任何事件禁止携带:
1. **宠物名、品种自由文本**:物种用 `species` 枚举(`cat`/`dog`/`other`),品种不上报。
2. **档案自由文本**:备注、症状描述、提醒文案原文。
3. **精确数值**:体重公斤数、花费金额、疫苗批号。
4. **媒体线索**:照片 URL、文件名、本地路径(只允许 `photoCount` 整数)。
5. **路由参数**`page_viewed.pageName``referrer` 必须是归一化枚举——`/pet/3f8a…` 一律归一为 `pet_detail`,禁止把宠物/记录 UUID 混进页面名。
红线正则(`password|token|secret|phone|mobile|email|credential|idfa|gaid`**本轮不扩**:加 `name`/`note` 类宽泛词会误伤 `pageName``recordType` 等合法字段;内容字段靠白名单剥离兜底,另新增值级巡检(§6.4)补防线。
### 1.4 新事件清单
`recordType` 枚举(多事件共用,对应 M2 四类记录接口):`weight` / `vaccine` / `health_event` / `reminder`
失败枚举基底(在 v1 的 `validation_error`/`rate_limited`/`network_error`/`server_error` 之上,按 M2 验收新增):
- `permission_denied` — 无权限访问宠物(403owner/caregiver/viewer 权限模型的观测点)
- `conflict` — 并发更新冲突(M2 验收「并发更新返回明确冲突」的观测点)
- `not_found` — 目标宠物/记录已被删除(多设备场景)
#### 宠物创建(pet 域)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `pet_create_started` | 用户进入建宠表单并产生**首次输入**(到达表单页由 `page_viewed(pageName=pet_form)` 承接,见 §1.6 修订),每次进入记一次 | `entryPoint``profile_empty_state` / `pet_list` / `post_register_guide`,枚举待 UI 定稿收敛) |
| `pet_create_succeeded` | 客户端收到建宠接口成功响应(code=0)后(**漏斗事件**) | `durationMs``species`(枚举)、`petIndex`(该用户第几只宠物,int,H2 假设的直接数据源) |
| `pet_create_failed` | 失败响应 / 超时 / 本地校验拦截 | `failureReason``errorCode`(可空)、`httpStatus`(可空)、`attemptSeq` |
`pet_create_failed.failureReason``validation_error``pet_limit_reached`(若产品设上限,**待拍板**:无上限则删此枚举)、`rate_limited``network_error``server_error`
> 说明:示例名 `pet_created` 不符合 v1「结果后缀」惯例,按 `<域>_<动作>_<结果>` 正名为 `pet_create_succeeded` 系列。
#### 健康记录创建(health_record 域)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `health_record_create_started` | 进入某类记录的创建表单并产生首次输入 | `recordType``entryPoint``pet_detail` / `record_list` / `reminder`,待 UI 定稿收敛) |
| `health_record_create_succeeded` | 收到创建接口成功响应后(**漏斗事件**,北极星与 H1/H3/H4 的核心数据源) | `recordType``durationMs``photoCount`int,无照片为 0 |
| `health_record_create_failed` | 失败响应 / 超时 / 本地校验拦截 | `recordType``failureReason``errorCode``httpStatus``attemptSeq` |
`failureReason``validation_error``permission_denied``not_found``rate_limited``network_error``server_error`
#### 记录浏览 / 编辑 / 删除
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `health_record_viewed` | 记录**详情**页可见(列表滚动曝光不算,防事件洪水) | `recordType``source``record_list` / `pet_detail` / `reminder` |
| `health_record_edit_succeeded` | 编辑保存成功响应后 | `recordType``fieldCount`(本次变更字段数,int,可空) |
| `health_record_edit_failed` | 编辑保存失败 | `recordType``failureReason`(含 **`conflict`**)、`errorCode``httpStatus` |
| `health_record_deleted` | 删除成功响应后(仿 `auth_logout` 单事件风格;删除失败不埋,靠服务端接口错误率观测) | `recordType` |
宠物列表/详情的**浏览**不设 `pet_viewed`——由 `page_viewed``pageName = pet_list` / `pet_detail`)覆盖,避免双事件重复计数。编辑不设 `started`:短表单,started→succeeded 漏斗价值低于事件成本;若编辑放弃率成为问题再以 eventVersion=2 增补。
### 1.5 v2 增量总览(10 个新事件 + 1 转正 + 1 废弃)
| # | 事件名 | 版本 | 性质 |
| --- | --- | --- | --- |
| 12 | `pet_create_started` | 1 | 新增 |
| 13 | `pet_create_succeeded` | 1 | 新增(漏斗事件) |
| 14 | `pet_create_failed` | 1 | 新增 |
| 15 | `health_record_create_started` | 1 | 新增 |
| 16 | `health_record_create_succeeded` | 1 | 新增(漏斗事件) |
| 17 | `health_record_create_failed` | 1 | 新增 |
| 18 | `health_record_viewed` | 1 | 新增 |
| 19 | `health_record_edit_succeeded` | 1 | 新增 |
| 20 | `health_record_edit_failed` | 1 | 新增 |
| 21 | `health_record_deleted` | 1 | 新增 |
| — | `page_viewed` | 1 | 转正(工单增补 → 字典正稿,pageName 枚举化) |
| — | `health_record_action` | — | **废弃**(从未启用,后端白名单直接移除,见 §1.2) |
后端 `EventDictionary` 白名单增量(工单可直接抄):
```java
Map.entry("pet_create_started", Set.of("entryPoint")),
Map.entry("pet_create_succeeded", Set.of("durationMs", "species", "petIndex")),
Map.entry("pet_create_failed",
Set.of("failureReason", "errorCode", "httpStatus", "attemptSeq")),
Map.entry("health_record_create_started", Set.of("recordType", "entryPoint")),
Map.entry("health_record_create_succeeded", Set.of("recordType", "durationMs", "photoCount")),
Map.entry("health_record_create_failed",
Set.of("recordType", "failureReason", "errorCode", "httpStatus", "attemptSeq")),
Map.entry("health_record_viewed", Set.of("recordType", "source")),
Map.entry("health_record_edit_succeeded", Set.of("recordType", "fieldCount")),
Map.entry("health_record_edit_failed",
Set.of("recordType", "failureReason", "errorCode", "httpStatus")),
Map.entry("health_record_deleted", Set.of("recordType"))
// 同时删除 Map.entry("health_record_action", ...) —— 从未启用,见 §1.2
```
Flutter 侧沿用报告 13 §3.1 的强类型封装惯例:新建 `pet_analytics.dart` / `health_record_analytics.dart`,枚举编译期锁死,业务代码禁止手拼事件名与属性。
### 1.6 漏斗闭环与维度够用性复核(本角色新增)
复核方法:以 §3 的 4 条假设 + §2 全部指标逐条反推数据源,凡定义式引用了字典中不存在的事件/属性即判缺口。结论如下。
**闭环成立**`pet_create``health_record_create` 两条漏斗均有 started → succeeded / failed 配对,失败枚举覆盖 M2 验收要求的权限(`permission_denied`)与并发(`conflict`)场景,闭环判定通过。编辑不设 started、删除不埋失败,属自觉取舍,同意(复活条件已在 §1.4 注明)。
**修订 1(实质缺口,本版已修)**:初版 pageName 枚举为 `login / register / home / profile / pet_list / pet_detail / record_form / record_detail`**缺建宠表单页**。`pet_create_started` 定义在「首次输入」触发,意味着「到达表单即放弃」的人群只能靠 page_viewed 兜住——枚举里没有建宠表单页名,建宠漏斗的「到达 → 动笔」段就不可测,完成率分母系统性偏小、读数虚高。**修订:pageName 枚举增补 `pet_form`**,建宠漏斗三段式为 `page_viewed(pet_form) → pet_create_started → pet_create_succeeded`;健康记录漏斗同理由 `record_form` 承接到达段(初版已有此页名,无需改)。
**缺口 2(接受不埋)**:宠物编辑/删除无事件——低频管理动作,不构成漏斗,服务端事实表可查,不埋。
**缺口 3(接受不埋,有条件)**:提醒完成/忽略(pending→completed/dismissed)无事件。H3 的验证只需「提醒创建」(`recordType=reminder`)与回访事件,均已具备;提醒完成率从 `care_reminders` 事实表(`status`/`completed_at`)出数即可。**条件**:若 M3+ 要做提醒推送类实验,届时必须增补 `reminder_completed` 事件(记入字典 backlog),因为推送实验的主指标需要客户端行为时序而非仅终态。
**维度够用性**H1 需 `recordType`(有);H2 需 `petIndex`(有,另以 `pet.pets` 事实表交叉验证);H3 需 `recordType=reminder` 分群 + userId 时序(有);H4 需 `pet_create_succeeded``health_record_create_succeeded` 的 userId + serverTs(有)。**全部假设可由本字典 + M2 事实表回答,维度判定通过。**
---
## 2. M2 指标体系(北极星定义式落定 + 漏斗 + 护栏)
统计口径沿用 v1`serverTs` 划 UTC 日界;主体去重用 `userId`M2 事件全部发生在登录后,`anonymousId` 兜底理论上不该出现——出现即数据质量信号,§6.5 巡检)。
### 2.1 北极星:7 日回访记录率(本角色裁定采用候选 A,定义式落定)
初版将 A/B 二选一列为待拍板且只给了方向性描述。本角色以实验专业裁定:**采用 A「7 日回访记录率」为北极星,B「档案激活率」降级为辅助漏斗指标**(保留 PM 否决权,见 §8)。理由:健康档案的产品价值在「持续记录」而非「一次性录入」;A 是留存型指标,难被一次性强引导冲高,B 恰恰易被冲高从而与长期价值背离——B 适合做诊断,不适合做方向。
初版定义(「首次成功后 7 个自然日内再次 ≥1 条」)存在三处不可操作的模糊:同日批量录入算不算回访?窗口从时刻算还是从日界算?分母是哪个「首次」?本版落定如下。
**定义式**
```
7日回访记录率(w) =
| { u : firstRec(u) ∈ 周 w,且 ∃ e ∈ E(u)day(e) ∈ [day(firstRec(u))+1, day(firstRec(u))+7] } |
─────────────────────────────────────────────────────────────────────────────
| { u : firstRec(u) ∈ 周 w } |
```
口径逐项:
| 要素 | 落定口径 | 理由 |
| --- | --- | --- |
| firstRec(u) | 用户 u **平台生命周期内首条** `health_record_create_succeeded``server_ts`(min),非「本周期首条」 | 回访衡量习惯养成,只对真正的新记录用户有意义 |
| 分母 | firstRec 落在 ISO 周 wUTC)内的去重 `userId``user_id IS NULL` 的事件不计入(应为空集,§6.5 兜底) | 按首记周分队列,队列间互斥 |
| 分子 | 分母中,在 **day(firstRec)+1 至 day(firstRec)+7**(UTC 自然日,**不含首记当日**)内再产生 ≥1 条 `health_record_create_succeeded` 者;任意 `recordType`、任意宠物均算 | **排除首记当日**是关键:不排除则首次使用时同会话批量录入 3 条体重也算「回访」,指标失去留存含义 |
| 回访事件范围 | 仅创建成功事件;`viewed`/`edit` 不算回访 | 北极星衡量「持续产生记录」,浏览是弱得多的信号,混入会稀释 |
| 删除处理 | 记录事后被删不影响计数(行为已发生) | 事件表不可变语义 |
| 队列成熟期 | 队列须等到 day(firstRec)+8(UTC)才可出数;未成熟队列不发布 | 防止半熟队列读数系统性偏低 |
| 去重 | 全程 `userId`;多设备同账号合并计 | 会话/设备维度不参与——刻意使北极星**不依赖 sessionId**(偏差 1 修复与否不污染北极星) |
**出数 SQL(巡检脚本可直抄)**
```sql
WITH first_rec AS (
SELECT user_id,
date_trunc('day', min(server_ts) AT TIME ZONE 'UTC') AS first_day,
date_trunc('week', min(server_ts) AT TIME ZONE 'UTC') AS cohort_week
FROM platform.product_events
WHERE event_name = 'health_record_create_succeeded' AND user_id IS NOT NULL
GROUP BY user_id
),
returned AS (
SELECT DISTINCT f.user_id
FROM first_rec f
JOIN platform.product_events e
ON e.user_id = f.user_id
AND e.event_name = 'health_record_create_succeeded'
AND date_trunc('day', e.server_ts AT TIME ZONE 'UTC')
BETWEEN f.first_day + interval '1 day' AND f.first_day + interval '7 day'
)
SELECT f.cohort_week,
count(*) AS cohort_users,
count(r.user_id) AS returned_users,
round(100.0 * count(r.user_id) / count(*), 2) AS return_rate_pct
FROM first_rec f
LEFT JOIN returned r USING (user_id)
WHERE f.first_day + interval '8 day' <= date_trunc('day', now() AT TIME ZONE 'UTC') -- 只出成熟队列
GROUP BY f.cohort_week
ORDER BY f.cohort_week;
```
**统计纪律**:早期周队列样本小,读数按 Wilson 95% 置信区间发布(不裸报点估计);队列人数 < 50 的周与相邻周合并或改用 4 周滚动口径,禁止对小样本周环比做趋势解读。
**辅助指标 B(档案激活率,降级为诊断漏斗)**:当周新注册用户中,完成「建宠 + ≥1 条健康记录」全链路的比例(事件表 + `identity.users`)。读数即时,用于诊断激活链路(配合 H4),不作方向指标。
### 2.2 漏斗指标(随埋点上线即产出)
- **建宠三段漏斗**(§1.6 修订后):`page_viewed(pet_form)``pet_create_started``pet_create_succeeded`,各段按去重 userId、24 小时归因窗(v1 注册转化率同款口径)。「到达→动笔」流失指向入口与表单首屏,「动笔→成功」流失指向表单项与校验。
- **档案创建完成率** = `health_record_create_succeeded` / `health_record_create_started`,同口径,按 `recordType` 拆分——哪类表单流失最重是 UI 迭代的直接输入(`record_form` 到达段同理三段化)。
- 辅助:`*_create_failed``failureReason` 分布(`validation_error` 高 → 表单/文案问题;`network_error`/`server_error` 高 → 技术问题)。
### 2.3 护栏指标(M2 期间任何改动不得劣化)
| # | 护栏 | 口径 | 阈值(**待拍板**) |
| --- | --- | --- | --- |
| 1 | 并发冲突率 | `health_record_edit_failed(failureReason=conflict)` / 编辑尝试总数(= edit_succeeded + edit_failed | 建议 < 1%;持续高于阈值说明乐观锁粒度或客户端刷新策略有问题(对应 M2 验收「并发更新返回明确冲突」) |
| 2 | 越权信号 | `permission_denied` 事件数(绝对值) | 期望≈0;任何持续非零都是权限模型或客户端入口控制回归,P1 排查 |
| 3 | M1 存量指标不回退 | 登录成功率、会话恢复成功率(v1 §2.2/2.3 口径) | 不低于 M2 开工前 2 周基线均值 − 2pp |
| 4 | 埋点自身健康 | 事件丢失率 < 5%、对账偏差 < 5%(§6)、去重命中率 < 10% | 沿用 v1 实验前置条件阈值 |
| 5 | 崩溃率 | **暂缺采集手段**(无崩溃上报 SDK,引第三方违反 v1「不绑定未评审供应商」约束) | 占位待拍板:M2 是否接受用「会话异常中断率」(§5.1 sessionId 落地后可推算)代偿 |
---
## 3. M2 产品假设(本角色新增,可证伪,上线前登记)
**方法约定**:以下阈值是**上线前登记的判定线,不是 KPI**——判定线先于数据存在,防止事后看图说话(HARKing)。每条假设的观察窗口届满即出判定,三种结局:支持 / 证伪 / 数据不足(样本未达最低量,顺延一个窗口并注明)。所有假设的数据源都已在 §1.6 验证「字典可答」。上线第 1 周为尝鲜噪声期,除 H4 外一律剔除。
### H1:体重是最高频的记录类型(信息架构假设)
- **陈述**:稳定期内,`weight` 在四类记录的创建量中占比第一且 ≥ 35%。
- **判定指标**`health_record_create_succeeded``props->>'recordType'` 的分布占比(与 §6.2 对账 SQL 的 evt_side 同源;以事实表侧交叉验证)。
- **判定线**:支持 = weight 第一且 ≥ 35%;证伪 = 连续 4 周 weight 非第一,或占比 < 25%;中间地带 = 顺延观察。
- **窗口**:上线后第 2–5 周。
- **行动**:支持 → 记录入口默认落体重、快捷录入优化优先投给体重表单;证伪 → 按实际头部类型重排入口与 M3 表单优化优先级。
### H2:用户会为多只宠物建档(多宠价值假设)
- **陈述**:有宠用户中,拥有 ≥ 2 只宠物档案的占比 ≥ 20%。
- **判定指标**:主数据源为 `pet.pets` 事实表(按 owner 去重计宠物数——事实表无丢失率,作分布真值);`pet_create_succeeded.petIndex` 的 per-user 最大值作事件侧交叉验证。
- **判定线**:支持 = ≥ 20%;证伪 = < 10%1020% 顺延。
- **窗口**:上线后 4 周末读数。
- **行动**:支持 → 宠物切换器/多宠列表体验进 M3 优先级;证伪 → 多宠管理 UI 降级,`petIndex` 维度保留继续观察。
### H3:创建提醒的用户回访记录率更高(提醒价值假设)
- **陈述**:首记后 7 日内创建过 ≥ 1 条 `reminder` 类记录的用户,其 7 日回访记录率比未创建者高 ≥ 10pp。
- **判定指标**:§2.1 北极星 SQL 按「窗口内是否有 `recordType='reminder'` 的创建成功事件」分成两群,比较回访率之差(回访事件计算时**剔除 reminder 类型自身**,防止「建了提醒」同时既定义分群又充当回访,循环论证)。
- **判定线**:支持 = 差值 ≥ 10pp 且两群各 ≥ 100 人;证伪 = 差值 < 5pp 或倒挂;510pp 顺延。
- **窗口**:上线后 6 周(需 ≥ 2 个成熟队列)。
- **方法论警示**:这是**观察性对照,只能证明相关**——爱记录的用户本来就更可能建提醒(自选择偏差)。支持结论的正确用法不是宣布因果,而是把「默认引导创建提醒」列为**首个 A/B 实验候选**(§4.3),用随机化坐实因果后再全量。
- **行动**:支持 → 进 A/B 候选池;证伪 → 提醒功能保持工具定位,不投入引导资源。
### H4:建宠后会立即产生首条记录(激活链路假设)
- **陈述**:完成建宠的用户中,≥ 50% 在建宠后 24 小时内产生第一条 `health_record_create_succeeded`
- **判定指标**per user 的首次 `pet_create_succeeded` 与首次 `health_record_create_succeeded``server_ts` 差值分布中,≤ 24h 的占比。
- **判定线**:支持 = ≥ 50%;证伪 = < 30%3050% 顺延。
- **窗口**:上线后 4 周(含第 1 周——激活链路恰恰要看新用户首触行为)。
- **行动**:证伪 → 说明建宠成功页缺少「顺手记一笔」的引导落点,「建宠成功页引导首条记录」进 A/B 候选池(与 H3 候选竞争首实验席位,配合辅助指标 B 诊断);支持 → 激活链路健康,优化资源全部投向回访(北极星)。
---
## 4. A/B 实验:M2 不启动(判断成立),启动路线首次给出
### 4.1 M2 不启动的复核结论
初版判断**成立**:v1 前置条件截至今日一项未变绿——指标基线连一天真实数据都没有,此时分流实验只会产出噪声结论。M2 的正确动作是把漏斗测准、把 §3 的假设判定跑起来(观察性分析不需要分流基础设施)。但「不做」不等于「不规划」,前置条件与达成路线如下。
### 4.2 前置条件清单 × 预计达成迭代
| # | 前置条件 | 内容 | 责任侧 | 预计达成 |
| --- | --- | --- | --- | --- |
| 1 | 数据质量验收 | 丢失率 < 5%、对账偏差 < 5%、去重命中 < 10%、serverTs 覆盖 100%、无红线泄漏 | 数据(§6 巡检即验收手段) | **M2 内**(埋点上线 + 2 周巡检) |
| 2 | 指标基线 | §2 指标连续稳定产出 ≥ 2 周,形成均值与方差,与服务端日志交叉核对一致 | 数据 | **M2 末–M3 初** |
| 3 | 样本量规则成文 | 给定基线率、MDE、95% 置信度、80% 功效的样本量计算方法与查表;按实际 DAU 换算实验最短运行时长 | 本角色(纯文档) | **M3** |
| 4 | 稳定分流组件 | `hash(userId, experimentSalt) % buckets`,实验期内分组不变、跨端一致;登录前实验用 `anonymousId` 并定义登录后归并规则 | 后端 | **M3** |
| 5 | 曝光事件 | `experiment_exposed(experimentKey, variant)` 进字典;分析只统计实际曝光用户,杜绝按分配名单算分母 | 后端 + Flutter | **M3**(随 #4 |
| 6 | 实验设计模板与评审流程 | 假设、主指标、护栏、提前停止规则、多重比较校正约定 | 本角色(模板可先行) | **M3** |
| 7 | 护栏监控与回滚 | 护栏指标准实时监控 + feature flag 一键回滚 | 后端/DevOps | **M3M4** |
| 8 | 隐私合规复核 | 实验分组数据同守红线 | 每实验各一次 | 常态 |
**结论:M3 末 8 项可全绿,M4 具备启动首个 A/B 的条件。**
### 4.3 首实验候选与样本量现实检验
候选按 §3 判定结果二选一:H3 支持 → 「新用户默认引导创建提醒」;H4 证伪 → 「建宠成功页引导首条记录」。两者主指标都直接挂北极星或其激活前置,护栏用 §2.3 全套。
样本量现实检验(启动前必须重算,此处给数量级感):若激活率基线 40%、检出 +8pp 绝对提升、双侧 α=0.05、功效 80%,每组约需 600 个新建档用户,合计 ~1,200;以回访率(基线假设 25%、MDE +8pp)为主指标则每组约需 ~640,且每人多等 8 天成熟期。**若按届时 DAU 换算实验需运行超过 8 周,判定该实验不可行**,退回观察性分析并继续攒流量——这条止损线与实验本身一起在设计文档里预登记。
---
## 5. 两个遗留高优项的验收标准与对账方法
这两项是 M2 埋点数据可信的**前置**,排入 M2 第一波工单(先于档案功能挂接)。
### 5.1 sessionId 生命周期(session_tracker + WidgetsBindingObserver
现状:`analytics_service.dart` 第 55 行每事件 `const Uuid().v4()`,会话维度完全不可用(§0.1 偏差 1)。
**验收标准(全部满足才算关单)**
1. 新建 `lib/analytics/session_tracker.dart`,注册为 `WidgetsBindingObserver``AnalyticsService` 从它读 sessionId,删除每事件生成逻辑。
2. 语义三条(即报告 13 §4.0 定义):冷启动生成新 sessionId;`paused → resumed` 间隔 **> 30 分钟**生成新 sessionId**≤ 30 分钟**沿用原值。
3. 同一前台会话内产生的所有事件(跨不同 eventNamesessionId 完全一致。
4. sessionId 为 UUID,不落任何持久化存储(会话本该跨冷启动失效;`lastActiveAt` 时间戳可持久化用于判定,报告 13 §3.3 键位已预留)。
5. 单元测试 ≥ 3 例:冷启动新值 / 短后台沿用 / 长后台(注入时钟模拟 31 分钟)换新值。
6. 真机手测脚本:登录 → 退后台 5 分钟 → 回前台操作 → 退后台 35 分钟 → 回前台操作,库内应恰好出现 **2 个** sessionId,且切分点在长后台处。
**对账方法(上线后每日巡检 SQL,见 §6.3.1)**:每 sessionId 平均事件数。修复前该值恒等于 1;修复后应明显 > 1。告警口径:`distinct sessionId / 事件总数 > 0.9` 持续一天 = 生命周期逻辑未生效或回退。
### 5.2 page_viewed 路由埋点(RouteObserver
**验收标准**
1. `RouteObserver` 注册进 `MaterialApp.navigatorObservers``didPush`(含 `didPopNext` 返回露出)触发 `page_viewed`
2. `pageName` 是**编译期枚举**,v2 初始集合:`login` / `register` / `home` / `profile` / `pet_list` / `pet_detail` / **`pet_form`**(§1.6 修订新增)/ `record_form` / `record_detail`(随 M2 页面定稿增删,进字典说明);带参数路由必须归一化——任何 UUID/ID 出现在 pageName 或 referrer 中即验收失败(§1.3 红线第 5 条)。
3. `referrer` = 前一页 pageName,栈底/冷启动首页为 null。
4. 不在字典枚举内的路由(如 dialog、临时调试页)**不上报**,而不是报未知名(后端会整条 rejected,白白消耗队列)。
5. 单测/widget 测试:push 两页断言两条事件且 referrer 链正确;pop 返回断言 `didPopNext` 补报。
6. M1 存量四页(登录/注册/首页/个人中心)与 M2 新页一次性挂全。
**对账方法(§6.3.2**:两条 sanity 关系式——(a) 每个 sessionId 至少 1 条 `page_viewed`(进过 app 必然看过页面);(b) `page_viewed(pageName=login)` 日次数 ≥ `auth_login_succeeded + auth_login_failed` 的去重 sessionId 数(登录尝试必先到达登录页)。偏差持续 > 5% 告警。
---
## 6. 对账 SQL 草案 v2 增量
v1 的 5.2.1–5.2.5(登录/注册/刷新对账、红线扫描、技术指标)继续每日跑,本节只列**新增**。真值来源:M2 后端事实表。**表名以 M2 后端 DDL 定稿为准**,下文按开发计划域划分假定 `pet` schema`pet.pets``pet.weight_records``pet.vaccine_records``pet.health_events``pet.reminders`——若实际命名不同,替换表名即可,结构不变。
### 6.1 宠物创建对账
`pet_create_succeeded` 事件数 vs `pet.pets` 当日新建行数,UTC 日界,偏差 > 5% 告警(连续 2 日再升级,队列延迟说明同 v1 5.2)。
```sql
SELECT coalesce(p.day, t.day) AS day, coalesce(api_cnt, 0) AS api_cnt,
coalesce(evt_cnt, 0) AS evt_cnt,
round(abs(coalesce(evt_cnt, 0) - coalesce(api_cnt, 0))::numeric
/ greatest(coalesce(api_cnt, 0), 1) * 100, 2) AS diff_pct -- > 5 告警
FROM (SELECT date_trunc('day', created_at AT TIME ZONE 'UTC') AS day, count(*) AS api_cnt
FROM pet.pets GROUP BY 1) p
FULL JOIN (SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day, count(*) AS evt_cnt
FROM platform.product_events
WHERE event_name = 'pet_create_succeeded' GROUP BY 1) t USING (day)
ORDER BY day;
```
### 6.2 健康记录创建对账(按 recordType 分型)
事件侧按 `props->>'recordType'` 分组,真值侧四张事实表 UNION 后带类型标签,逐类型对账——单独一类偏差大能直接定位是哪个表单的挂接点漏报。该 SQL 的 api_side 分布同时就是 **H1 的真值侧读数**
```sql
WITH api_side AS (
SELECT day, record_type, count(*) AS api_cnt FROM (
SELECT date_trunc('day', created_at AT TIME ZONE 'UTC') AS day,
'weight' AS record_type FROM pet.weight_records
UNION ALL
SELECT date_trunc('day', created_at AT TIME ZONE 'UTC'), 'vaccine' FROM pet.vaccine_records
UNION ALL
SELECT date_trunc('day', created_at AT TIME ZONE 'UTC'), 'health_event' FROM pet.health_events
UNION ALL
SELECT date_trunc('day', created_at AT TIME ZONE 'UTC'), 'reminder' FROM pet.reminders
) u GROUP BY 1, 2
),
evt_side AS (
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
props->>'recordType' AS record_type, count(*) AS evt_cnt
FROM platform.product_events
WHERE event_name = 'health_record_create_succeeded'
GROUP BY 1, 2
)
SELECT coalesce(a.day, e.day) AS day, coalesce(a.record_type, e.record_type) AS record_type,
coalesce(api_cnt, 0) AS api_cnt, coalesce(evt_cnt, 0) AS evt_cnt,
round(abs(coalesce(evt_cnt, 0) - coalesce(api_cnt, 0))::numeric
/ greatest(coalesce(api_cnt, 0), 1) * 100, 2) AS diff_pct -- > 5 告警
FROM api_side a
FULL JOIN evt_side e ON a.day = e.day AND a.record_type = e.record_type
ORDER BY day, record_type;
```
### 6.3 两个遗留项的健康巡检(§5 对账方法的可执行形式)
**6.3.1 sessionId 生命周期生效性**
```sql
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
count(*) AS events,
count(DISTINCT session_id) AS sessions,
round(count(DISTINCT session_id)::numeric / greatest(count(*), 1), 3) AS session_ratio
FROM platform.product_events
GROUP BY 1 ORDER BY 1;
-- session_ratio 接近 1.0(每事件一会话)= sessionId 仍是每事件生成,未生效/回退,告警
-- 修复后预期显著 < 0.5(每会话多事件)
```
**6.3.2 page_viewed 覆盖率**
```sql
-- (a) 无 page_viewed 的会话占比(进过 app 必看过页面,期望≈0)
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
round(100.0 * count(DISTINCT session_id)
FILTER (WHERE session_id NOT IN (
SELECT session_id FROM platform.product_events WHERE event_name = 'page_viewed'))
/ greatest(count(DISTINCT session_id), 1), 2) AS pct_sessions_without_pv -- > 5 告警
FROM platform.product_events
GROUP BY 1 ORDER BY 1;
-- (b) 登录页浏览 ≥ 登录尝试会话数(sanity)
SELECT coalesce(pv.day, la.day) AS day, coalesce(pv_cnt, 0) AS login_page_views,
coalesce(attempt_sessions, 0) AS login_attempt_sessions
FROM (SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day, count(*) AS pv_cnt
FROM platform.product_events
WHERE event_name = 'page_viewed' AND props->>'pageName' = 'login' GROUP BY 1) pv
FULL JOIN (SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
count(DISTINCT session_id) AS attempt_sessions
FROM platform.product_events
WHERE event_name IN ('auth_login_succeeded', 'auth_login_failed') GROUP BY 1) la
USING (day)
ORDER BY day;
-- login_page_views < login_attempt_sessions 持续出现 = 路由埋点漏报,告警
```
### 6.4 内容泄漏值级巡检(红线 regex 不扩的补防线,见 §1.3)
白名单字段的**值**若出现长自由文本,说明有人把备注/宠物名塞进了合法字段名里:
```sql
SELECT event_name, k AS prop_key, count(*) AS hits
FROM platform.product_events
CROSS JOIN LATERAL jsonb_each_text(props) AS kv(k, v)
WHERE server_ts >= now() - interval '1 day'
AND length(v) > 64 -- 字典 v2 所有枚举/数值字段值长远小于 64
GROUP BY 1, 2;
-- 期望恒为空集;命中即 P1:核对该字段是否被塞入内容数据并清洗
```
### 6.5 M2 新事件的匿名兜底巡检
M2 事件全部发生在登录后,`user_id` 为 NULL 即挂接点在 `identify()` 之前触发或时序 bug(同时会污染北极星分母,见 §2.1):
```sql
SELECT event_name, count(*) AS null_user_rows
FROM platform.product_events
WHERE event_name LIKE 'pet_%' OR event_name LIKE 'health_record_%'
GROUP BY 1 HAVING count(*) FILTER (WHERE user_id IS NULL) > 0;
-- 期望空集(退出后补冲刷的历史队列除外,占比应 < 1%)
```
---
## 7. 埋点基础设施 M2 扩展性评估
**结论:接收端与存储零改动,客户端小修三处,无需任何架构扩展。**(复核初版量级估算,成立。)
### 7.1 量级估算(不需要扩容的依据)
- 单用户日事件量:auth 域 ~3–5 条 + `page_viewed` ~8–15 条(路由埋点补齐后的最大增量来源)+ pet/health_record 域 ~38 条 ≈ **1530 条/DAU/日**,约为 v1 的 34 倍。
- 接收端:正常客户端 30 秒一批、批上限 50 条,日 30 条远填不满一批;限流 60 请求/5 分钟余量依旧十几倍。**`/api/v1/events` 契约、限流、64KB 上限均不动。**
- 存储:即便 1,000 DAU × 30 条 × 365 天 ≈ 1,100 万行/年,距报告 13 §2.3 的 5,000 万行分区阈值仍有数年余量。**v1 不分区的决策继续有效。**
- 客户端队列:500 条上限可容纳两周以上的离线积压(30 条/日),**不调**。`page_viewed` 是新的高频事件,唯一注意点:列表页快速进出可能瞬时产生密集事件,§5.2 验收第 4 条(字典外路由不上报)+ 详情页曝光而非列表曝光(§1.4 `health_record_viewed` 触发时机)已从源头限流。
### 7.2 需要落的三处客户端小修(随 M2 第一波工单,均对应 §0.1 实锤偏差)
1. **sessionId 生命周期**(§5.1,P0——不修则 M2 全部会话维度指标作废)。
2. **eventId 改回 UUIDv7**:现行 `Uuid().v4()`(第 50 行)去重仍有效,但 v4 随机主键使 `platform.product_events` 插入丧失时间局部性,量级上来后 B-tree 写放大;`uuid` 包本就支持 v7,一行改动,顺手修。
3. **动态 appVersion/osVersion**(引 `package_info_plus`/`device_info_plus`,报告 19 遗留第 6 项):M2 起指标要按版本切片看回归,硬编码 `1.0.0+1` / `android-14` / `ios-17` 会让版本维度全体失真;与本波一起落。
### 7.3 后端唯一改动
`EventDictionary` 白名单增补 10 事件 + 移除 `health_record_action`(§1.5 代码块可直抄),加集成测试各一例(沿用报告 19 §5.1 接入流程)。无表结构、无契约变更。
---
## 8. 待拍板清单(汇总)
| # | 事项 | 选项 | 本角色裁定/建议 |
| --- | --- | --- | --- |
| 1 | 北极星指标 | A:7 日回访记录率 / B:档案激活率 | **已裁定 A**(定义式落定于 §2.1,B 降级辅助诊断;PM 保留否决权,否决须给替代定义式) |
| 2 | 产品假设 H1–H4 判定线 | §3 各阈值 | 上线前由 PM 会签一次,会签后**冻结**,窗口届满前不得修改(防事后画靶) |
| 3 | 护栏阈值 | 并发冲突率 < 1%?M1 指标回退容忍 2pp? | 按 §2.3 默认值先跑,两周数据后复核 |
| 4 | 崩溃率护栏 | 无采集手段:接受「会话异常中断率」代偿 or 排期评审崩溃 SDK | M2 用代偿,SDK 评审进 M6 交付加固 |
| 5 | `pet_limit_reached` 枚举 | 产品是否设单用户宠物数上限 | 无上限则从枚举删除 |
| 6 | `entryPoint`/`pageName` 枚举终稿 | 待 M2 UI 设计稿定稿后收敛(`pet_form` 为本版硬性新增,见 §1.6) | 埋点工单开工前由 UI + 本角色对齐一次 |
| 7 | `health_record_action` 移除 | 后端白名单直接删 vs 保留标 deprecated | **直接删**(零客户端引用已核实,零兼容成本,见 §1.2) |
| 8 | A/B 启动路线 | §4.2 八项前置 × 迭代 | M3 末全绿、M4 首实验;候选依 H3/H4 判定结果二选一 |
---
## 附:M2 埋点工单拆分建议(按依赖排序)
1. **Flutter P0 前置**session_tracker(§5.1+ eventId v7 + 动态设备信息(§7.2)——先于一切新事件。
2. **Flutter**`RouteObserver` + `page_viewed` 全页面挂接(§5.2,含 `pet_form`),M1 存量四页一并补齐。
3. **后端**`EventDictionary` v2 增量(§7.3)——可与 1、2 并行。
4. **Flutter**:档案功能开发时按 §1.4 挂接 10 个新事件(强类型封装先行)。
5. **数据**:§6 五组对账 SQL + §2.1 北极星 SQL 入巡检;上线首周每日人工看 §6.3 两项(遗留修复的生效性验证)。
6. **本角色**:H1–H4 判定线 PM 会签(拍板 #2)→ 冻结登记;M3 初产出样本量规则文档与实验设计模板(§4.2 #3#6)。
@@ -0,0 +1,198 @@
# 07 第二迭代开工前:证据基线审计(Evidence Baseline Audit
**审计人**Evidence Collector
**审计日期**2026-09-07
**审计范围**:第一迭代收官声称的证据链完整性 + M2 开工基线快照
**方法**:只读审计。每条结论附可复现命令与实际输出;本报告不重复运行测试套件(「现在还绿不绿」由 Reality Checker 独立验证),静态计数不等于运行结果。
---
## 0. 结论速览
| 声称 | 判定 | 证据 |
|---|---|---|
| 20 份报告入档并挂 mkdocs 导航 | ✅ 完全证实 | §1 |
| OpenAPI 契约正式化 | ⚠️ 部分证实(缺 `/api/v1/events` | §2.1 |
| ADR-001~008 编号完整 | ✅ 完全证实 | §2.2 |
| 三仓提交完整、工作区干净 | ✅ 完全证实 | §3 |
| 后端 82 测试 | ✅ 静态计数一致(82 个 `@Test` | §4.2 |
| 前端 34 测试 | ✅ 静态计数一致(34 个 `test/testWidgets` | §4.2 |
| E2E 烟囱测试 7/7 | ✅ 有档案证据(报告 18 全量输出 + 脚本入库) | §5.3 |
| CIGitea Actions)全绿 | ❌ 本地不可证(无归档 run 日志) | §5.1 |
**证据链完整率:8 大类声称中 6 项完全证实、1 项部分证实、1 项本地不可证 ≈ 81%。**
**证据缺口:2 个**(详见 §5)。**基线快照:已建立**(§4)。
---
## 1. 证据链审计:20 份报告与导航
### 1.1 文件存在性
```bash
ls /home/lx/workspace/patbond/patbond-doc/docs/development/iterations/iteration-1/ | sort
```
实际输出:`01-pm-task-breakdown.md``20-iteration-1-summary.md` 共 20 份,外加 `index.md`(进展看板),**21 个文件全部存在,无缺失**。
### 1.2 mkdocs 导航
```bash
grep -c "iterations/iteration-1/" patbond-doc/mkdocs.yml
# 输出:21
```
逐条核对 mkdocs.yml 第 12~32 行:进展看板 + 01~20 报告共 21 条导航,与文件一一对应。**报告-导航映射完整率 100%。**
---
## 2. 契约档案审计
### 2.1 openapi.yaml 接口路径
```bash
grep -nE "^ /" patbond-doc/docs/api/openapi.yaml
```
实际输出(5 条路径):
| # | 路径 | 行号 |
|---|---|---|
| 1 | `/api/v1/auth/register` | 54 |
| 2 | `/api/v1/auth/login` | 86 |
| 3 | `/api/v1/auth/refresh` | 126 |
| 4 | `/api/v1/auth/logout` | 159 |
| 5 | `/api/v1/me` | 188 |
与契约自述范围(`title: Patbond API — Auth & Me(第一批公开接口)``version: 1.0.0`)一致,也与 `docs/api/index.md` 声称的「5 个端点」一致。
**但与代码实际公开接口比对存在缺口**
```bash
grep -rhoE '@(Get|Post)Mapping\("[^"]*"' patbond-api --include="*.java" | grep -v target | sort -u
```
代码中的公开接口为 `/api/v1/auth/{register,login,refresh,logout}``/api/v1/me`,以及 **`POST /api/v1/events`(埋点批量上报,报告 13/19 交付,提交 6d47c5a)——此接口未入 openapi.yaml**。`docs/api/index.md` 明文约定「契约变更须先改 OpenAPI,再改实现(契约先行)」,events 接口违反了这条自定约定。判定:**契约档案部分完整**,M2 开工前应补录(或明确声明 internal/events 不在公开契约范围并记录该决定)。
另核实:`/internal/users/*``/internal/sessions/*` 为服务间内部接口,不入公开契约属合理范围。openapi.yaml 本身未直接挂 mkdocs 导航,但导航条目「API → 契约说明(api/index.md)」内有指向 openapi.yaml 的链接,mkdocs 构建会连带发布该文件,可接受。
### 2.2 ADR 编号完整性
ADR 实际位于 `docs/architecture/decisions.md`(注意:不在 development/ 目录下)。
```bash
grep -nE "^#+ .*ADR-[0-9]+" patbond-doc/docs/architecture/decisions.md
```
实际输出:ADR-001Spring Boot 3)、002(移除 Nacos)、003Token 策略)、004(账号密码登录)、005(品牌色正典)、006(测试容器化)、007(部署形态)、008(PostgreSQL 18),行号 8/20/42/51/55/65/75/88。**001~008 连续无断号,判定完整。**
---
## 3. 提交完整性审计
命令:`git -C <repo> log --oneline -20``git status --short --branch``git rev-list --left-right --count HEAD...@{u}`2026-09-07 执行)。
### 3.1 三仓状态
| 仓库 | 分支 | HEAD | 工作区 | 与 upstream 差异 |
|---|---|---|---|---|
| patbond-api | dev | `0d81c38` | 干净(porcelain 无输出) | 0 ahead / 0 behind |
| patbond-flutter | dev | `3f8388e` | 干净 | 0 ahead / 0 behind |
| patbond-doc | main | `5537f92` | 干净 | 0 ahead / 0 behind |
**未提交文件清单:三仓均为空。** 第一迭代收官时「仅 flutter 待提交」的遗留已闭环(flutter 现有 CI 门禁三提交 3f8388e/45f94d2/b0207c9 在 dev 且已推送)。
### 3.2 声称提交与 git 历史比对
第一迭代总结(报告 20)声称的关键提交均可在历史中找到实体:
- patbond-api:埋点接收端 `6d47c5a`、会话清理 `6528a06`、CI 工作流 `3f6e818` + 修复 `b38b0d8`/`0d81c38`、Compose `ab0265c`、JWT 纵切 `4dc3dcd`、Flyway baseline `bd20adc` ——全部在 dev 历史中。用户自有提交 `b22eaed update` 位于 `6528a06` 之后,属已知正常情况。
- patbond-flutter:埋点 `60d67a3`、登录纵切 `8d890c0`、主题迁移 `af002ed`、phone 可空修复 `845e92f`、锁定码映射 `da25804` ——齐全。
- patbond-doc:报告迁入 `209021e`、收官 `8e0e1c5`/`64521bf`、CI `f267141`/`5537f92`、ADR-007/008 入档 `b747e09`/`18746ce` ——齐全。
**判定:声称已提交的内容真实存在于 git 历史,无虚报。**
---
## 4. M2 开工基线快照(验收对比基准)
> M2 结束时以本节为基准做前后对比。所有数字均注明取证方式。
### 4.1 三仓 HEAD(完整哈希)
| 仓库 | 分支 | HEAD commit |
|---|---|---|
| patbond-api | dev | `0d81c38fc6f1ea5ede3ad93bef89046a67e818e5` |
| patbond-flutter | dev | `3f8388e5d4f6dfc9ddf832ed77ebae7e5463ece9` |
| patbond-doc | main | `5537f92227c0cbad812f83c4374e589afbb17cbc` |
### 4.2 测试数基线
**取证方式:静态注解计数(grep),非运行结果**;运行态验证以 Reality Checker 同期报告为准。
```bash
# 后端:82(与声称一致;无 @ParameterizedTest/@RepeatedTest
grep -rE "@Test\b" patbond-api --include="*.java" | grep -v "/target/" | wc -l
# 分模块:patbond-auth 31 / patbond-common 3 / patbond-user 48
# 前端:34(与声称一致,8 个测试文件)
grep -rE "^\s*(test|testWidgets)\(" patbond-flutter/test --include="*.dart" | wc -l
```
| 端 | 基线值 | 来源 |
|---|---|---|
| 后端测试 | **82**auth 31 + common 3 + user 48 | 静态计数,与报告 20 声称一致 |
| 前端测试 | **34**8 个 `*_test.dart`) | 静态计数,与报告 20 声称一致 |
| E2E 烟囱 | **7/7**(声称值) | 报告 18 归档输出,本次未重跑 |
前端测试文件清单:`test/analytics/analytics_service_test.dart``test/core/network/token_refresher_test.dart``test/core/widgets/app_text_field_test.dart``test/core/widgets/primary_button_test.dart``test/features/auth/{auth_repository,login_page,register_page}_test.dart``test/widget_test.dart`
### 4.3 OpenAPI 接口基线
`patbond-doc/docs/api/openapi.yaml`OpenAPI 3.0.3version 1.0.0)共 **5 条路径**`/api/v1/auth/register``/api/v1/auth/login``/api/v1/auth/refresh``/api/v1/auth/logout``/api/v1/me`。代码另有公开接口 `POST /api/v1/events` 未入契约(见 §5 缺口 1)。
### 4.4 Flyway 迁移基线
```bash
find patbond-api -path "*src/main*db/migration*" -name "*.sql" | sort
```
| 版本 | 文件(patbond-user 模块) |
|---|---|
| V1 | `V1__identity_media_baseline.sql` |
| V2 | `V2__create_platform_product_events.sql` |
**M2 的健康档案表迁移应从 V3 起编号。**
### 4.5 CI 与其他基线
- 三仓均存在 `.gitea/workflows/ci.yml`api 2705B / flutter 2362B / doc 1038B),随 HEAD 入库。
- ADR 基线:ADR-001~008M2 新决策从 ADR-009 起。
- E2E 脚本 `test_e2e_manual.dart` 已入 patbond-flutter git 追踪(位于仓库根目录而非 test/,见 §5 备注)。
---
## 5. 证据缺口清单
### 缺口 1(中):`POST /api/v1/events` 未入 OpenAPI 契约
- **声称**:「OpenAPI 契约正式化」(报告 20);`api/index.md` 约定契约先行。
- **现实**:契约仅覆盖 Auth & Me 5 端点;events 为已上线公开接口(提交 6d47c5a)但契约中不存在。
- **建议**M2 第一波补录 events 到 openapi.yaml,或以 ADR/契约说明明文排除并给出理由。
### 缺口 2(中):CI「全绿」无本地可复现证据
- **声称**:报告 20「ci.yml #6 全绿 3m18s」(细节具体,可信度中上)。
- **现实**run 日志/截图未归档入 patbond-doc,本审计在本地仅能证实 ci.yml 文件存在,无法证实运行结果;需登录 Gitea 实例查看 Actions 页面方可复核。
- **建议**:后续迭代收官时将关键 CI run 的结论页截图或日志摘要归档入迭代报告,使该声称离线可验。
### 备注(低,非缺口)
1. ADR 实际路径为 `docs/architecture/decisions.md` 而非 development/ 下——引用时注意路径,内容本身完整。
2. `test_e2e_manual.dart` 放在 patbond-flutter 仓库根目录,不在 test/ 目录、不被 `flutter test` 纳入——属工程卫生问题,M2 可顺手归位。
3. openapi.yaml 未单列 mkdocs 导航,经 `api/index.md` 链接可达,可接受。
4. 本报告写入的 iteration-2 目录尚未挂 mkdocs 导航(本审计按约束不改 mkdocs.yml),待 doc 维护者统一挂载。
---
**结论**:第一迭代档案质量整体扎实——报告、导航、ADR、git 历史四条证据链均经实证核对无虚报;测试数静态计数与声称精确一致。两个缺口(events 契约缺录、CI 结果不可离线复核)均为可修补的档案问题,不阻塞 M2 开工。基线快照(§4)自本日起生效,M2 验收时据此对比。
@@ -0,0 +1,128 @@
# 08 M2 Git 与 CI 工作流规划
- 执行人:Git Workflow Master
- 日期:2026-09-07
- 范围:第二迭代(M2 宠物健康档案)开工前的三仓状态核查、分支/提交策略、CI 扩展与防泄漏规划。**本报告只核查与规划,未改动任何代码、工作流或 mkdocs.yml。**
---
## 1. 三仓当前状态核查(2026-09-07 实测)
| 仓库 | 分支 | 相对 origin | 工作区 | stash | 最新提交 CI 状态 |
| --- | --- | --- | --- | --- | --- |
| patbond-api | dev | 同步(fetch 后确认) | 干净 | 无 | **success**`0d81c38`CI / backend-testrun 6 |
| patbond-flutter | dev | 同步 | 干净 | 无 | **success**`3f8388e`CI / flutter-gatesrun 11 |
| patbond-doc | main | 同步 | 干净 | 无 | **success**`5537f92`CI / docs-build |
CI 状态经 Gitea commit status API 逐仓核实,非转述。
**未提交内容清单:无。** 第一迭代「三仓改动长期未 commit」的教训在收官阶段已彻底闭环——包括上次报告中留给用户自决的 patbond-flutter README.md 也已入库。唯一例外是本报告文件本身(写入 patbond-doc 后为未跟踪状态),按第 3 节波次规则随下一波提交。
两处非阻塞的历史遗留(可选清理,**待拍板**):
- patbond-api 本地 `master` 分支(`ff876bc`)的上游 `origin/master` 已在远端删除(`branch -vv` 显示「丢失」)。本地分支可删:`git branch -D master`(确认无独有提交后执行;`ff876bc` 是初始 README 提交,早已被 dev 包含的话可安全删除,删前用 `git merge-base --is-ancestor ff876bc dev` 核实)。
- patbond-flutter 本地 `main``030b11f`)与远端 `origin/main` 均落后于 dev。dev 是事实集成分支,main 处于闲置态。M2 不动它;若未来引入「main = 可发布」语义(见 2.3),届时再统一处理。
## 2. M2 分支与提交策略
### 2.1 现状评估
第一迭代的 trunk-based 小步直推 devdoc 直推 main)配合本地门禁运转良好:历史线性、无合并冲突、每个提交自带验收证据。但当时 CI 尚未上线,「门禁不绿不提交」全靠自觉;现在三仓 CI 已在 push 时执行同一套门禁,且**三个 ci.yml 均已配置 `pull_request:` 触发器**——PR 合入前门禁是零成本就绪的,只差用不用。
### 2.2 推荐方案(**待拍板**):trunk-based 为主 + 高风险改动走 PR
两人 + AI 辅助的协作模式下,日常改动走 PR 的评审收益低、流程开销高,不推荐全面切换。推荐分层:
- **日常改动**(单波次内可完成、不动 schema、不动跨仓契约):**继续小步直推 dev**(doc 直推 main)。CI 在 push 后兜底,红了立即修——两人团队里一个红提交的传播面可控。
- **高风险改动强制走短命分支 + Gitea PR**,合入前 CI 必须绿。触发条件(满足其一):
1. 新增/变更 Flyway 迁移(M2 的宠物健康档案必然新增 `V3__*.sql`,首当其冲);
2. 跨仓契约变更(openapi.yaml 的破坏性修改);
3. 依赖升级、大规模重构;
4. 两人同时改同一仓库的并行期。
- 分支命名沿用规范:`feat/<主题>``fix/<主题>`(如 `feat/pet-health-schema`),合入后即删,不留长期分叉。
- 个人分支整理历史用 `git push --force-with-lease`;共享分支(dev/main)依旧禁止 force push、禁止改写已推送历史。
选择理由:这是对现行 `git-workflow.md` 第 9 行「何时开 feature 分支」条款的最小延伸——把「破坏性风险」具体化为可判定的清单,并利用已就绪的 PR 触发器让 CI 在合入前把关,而不是引入一套全新流程。
**配套(可选,待拍板)**:在 Gitea 仓库设置中为 dev/main 开启分支保护,勾选「合并前需状态检查通过」并选中 CI 上下文。两人团队可以不开(靠约定),开了则规则由平台强制执行,AI 辅助开发场景下多一道机械防线。
### 2.3 暂不引入的东西
- 不引入 Git Flow / develop-release 双轨——没有版本化发布压力,dev 单集成分支足够。
- 不引入 main 发布分支语义——等 M3 有部署目标后再议。
## 3. M2 提交节奏规范
### 3.1 波次即提交(第一迭代教训的制度化)
- **每个波次收尾时,三仓凡有改动必须 commit 并 push,push 后确认 CI 绿,才算波次闭环。** 波次报告中记录各仓提交哈希与 CI 结论(沿用第一迭代收官报告的做法)。
- 波次中途允许多次小提交(鼓励),但不允许波次结束时仍有未提交改动过夜。
- AI 会话结束前,执行者对三仓各跑一次 `git status`,把结果写进波次报告——「工作区干净」要有出处。
### 3.2 提交信息:沿用现行约定,不引入新格式
`git-workflow.md` 已固化的「`feat/fix/refactor/docs/test/chore` 前缀 + 中文主题 + 正文验收证据 + ADR 引用」在第一迭代全程执行良好(近 20 个提交无一例外),**M2 原样沿用,不引入英文 conventional commits 或 scope 括号语法**——现行格式已具备 conventional commits 的全部实用价值(可 grep、可归类、可回溯),改格式只会割裂历史。
M2 补充一条:涉及契约的提交,正文注明对应的 openapi.yaml 版本或 doc 仓提交哈希(见 3.3)。
### 3.3 契约先行时的三仓提交顺序
M2 采用契约先行,顺序固定为:
1. **patbond-doc 先行**`docs/api/openapi.yaml` 的契约变更单独成提交(`docs: 宠物健康档案 API 契约(M2 波次 N)`),push 且 docs-build 绿。契约提交不与其他文档改动混杂,保证可独立引用与回退。
2. **patbond-api 跟进**:实现 + 测试成一或多个提交,正文引用 doc 仓契约提交哈希,push 且 backend-test 绿。
3. **patbond-flutter 收尾**:对接实现,正文同样引用契约哈希,push 且 flutter-gates 绿。
契约中途返工时,doc 仓允许在同波次内追加修订提交(契约未被下游消费前不算破坏性变更);一旦 api/flutter 已按某版契约合入,再改即视为破坏性修改,走 2.2 的 PR 通道。
## 4. CI 扩展规划
### 4.1 现状修正:任务假设的两问已被第一迭代末的事实回答
核查发现三仓 CI 均已上线且全绿,任务中「flutter 是否接入 CI」「doc 是否加 --strict 门禁」不再是开放问题:
- **patbond-flutter CI 已上线并验证可行**run 11 success)。零外部 action 约束下 Flutter SDK 进容器的方案已在 `ci.yml` 中落地:从 flutter-io.cn 镜像 curl 下载 Flutter 3.44.6 的 tar.xz,解压到挂载的 `gitea_toolcache` 卷(runner `container.options` 配置 `-v gitea_toolcache:/opt/hostedtoolcache`),首跑下载约 900MB,后续 run 复用缓存秒级就绪;pub 走 pub.flutter-io.cn。门禁为 format/analyze/test 三命令,与本地一致。
- **patbond-doc 的 `mkdocs build --strict` 门禁已上线**apt 装 mkdocs,规避 PEP 668docs-build success)。
- patbond-api CI 全绿(82 测试,约 3m18s),Testcontainers 经 docker.sock 挂载正常工作。
### 4.2 M2 的 CI 增量(按优先级,均为规划,实施时再改文件)
1. **无必做项。** 三条流水线覆盖了全部本地门禁,M2 开工不被 CI 阻塞。
2. 可选——**Flutter 版本升级流程注明**toolcache 以 `flutter-3.44.6` 目录名区分版本,升级 SDK 时改 ci.yml 中 `FLUTTER_VERSION` 即自动触发新版本下载,旧目录需手动清理卷(写入 ci-runner-setup.md 的常见问题即可,M2 内低优先)。
3. 可选——**api CI 增加 M2 迁移的守护**`./mvnw clean test` 已覆盖 Flyway 迁移执行(Testcontainers 起真库跑迁移),无需新增步骤;只需坚持「已推送迁移不可变」规则。
4. 明确**不做**flutter `build apk` 冒烟(耗时大、M2 无发布需求)、覆盖率门槛(先积累基线再谈阈值)。
## 5. 敏感信息防泄漏(轻量方案规划,待拍板后实施)
现状:三仓 `.git/hooks` 均只有样例,无任何自动检查;卫生完全靠 `git-workflow.md` 约定 + 提交前人工核对。api 仓敏感配置已按 `*.sample` 模式管理(真实 `application.yml` 在 gitignore 中)。AI 辅助开发下,机械防线值得补上。零外部 action 约束下推荐两层,均为纯 shell + grep,无任何外部依赖:
### 5.1 第一层:入库的共享 pre-commit 脚本(推荐先做)
- 各仓新增 `scripts/hooks/pre-commit`(入库,可评审、可演进),检查 `git diff --cached` 的暂存内容:
- **文件名黑名单**:拦截 `application.yml`(非 .sample)、`.env``*.pem``*.p12``*.jks``key.properties` 等入暂存区;
- **内容模式**:对暂存 diff 的新增行 grep 常见凭据特征——`BEGIN (RSA |EC )?PRIVATE KEY``password:`/`secret:` 后跟非占位值(排除 `changeme``your-*``<placeholder>` 等样例值)、长 base64/hex token 形态;
- 命中即拒绝提交并打印命中行号(不打印命中内容全文,避免终端留痕)。
- 启用方式为一次性 `git config core.hooksPath scripts/hooks`(每仓每机各执行一次,写入各仓 README)。hook 可被 `--no-verify` 绕过——这是特性不是缺陷:误报时有出口,且第二层兜底。
### 5.2 第二层:CI 侧兜底 grep(各仓 ci.yml 加一个 step
- checkout 后加一个纯 shell step,对整棵工作树跑同一套文件名/内容模式检查(复用 5.1 的脚本,保证两层规则同源),命中则 fail。零外部 action,新增耗时秒级。
- 与 pre-commit 的分工:hook 拦「即将提交的」,CI 拦「已经提交的」(含 `--no-verify` 绕过和历史遗漏的新暴露)。CI 只查工作树而不扫全历史——扫历史属一次性审计,若做一次即可,不进流水线。
### 5.3 不推荐
- gitleaks/trufflehog 等外部工具:与零外部依赖约束冲突(需拉二进制或镜像),且对本项目的敏感面(一个 application.yml + 未来的第三方 key)而言是牛刀。
- 提交后自动改写历史清除泄漏:一旦真泄漏,正确动作是**立即轮换凭据**,再考虑历史清理——写入规范备忘即可。
## 6. 待拍板事项汇总
| # | 事项 | 推荐 | 见 |
| --- | --- | --- | --- |
| 1 | M2 分支策略:trunk-based 为主 + 高风险改动(Flyway 迁移/契约破坏性变更/依赖升级/并行期)强制短命分支 + PR | 采纳 | 2.2 |
| 2 | Gitea dev/main 分支保护 + 状态检查强制 | 可选,倾向开启 | 2.2 |
| 3 | 提交信息格式沿用现行中文约定,不切换英文 conventional commits | 沿用 | 3.2 |
| 4 | 契约先行三仓提交顺序:doc → api → flutter,契约提交独立成提交并被下游引用 | 采纳 | 3.3 |
| 5 | 防泄漏两层方案(共享 pre-commit 脚本 + CI 兜底 grep | 采纳,M2 第一波实施 | 5 |
| 6 | patbond-api 本地孤儿 `master` 分支清理 | 顺手做 | 1 |
采纳后需要落实的文件改动(本报告未执行):各仓 `scripts/hooks/pre-commit` 与 ci.yml 的兜底 step、`git-workflow.md` 增补 2.2/3.1/3.3 条款、本报告挂入 mkdocs 导航。