# 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`