新增:体重录入与历史列表接入真实数据(T2-13 体重半边)

- WeightRecordsPage:cursor 分页(加载更多/末页收起/翻页失败保留重试)、
  loading/empty/error/retry 四态、下拉刷新;viewer 隐藏录入入口
- WeightFormPage:weightKg 契约区间 (0,500] 与两位小数前端校验 + 40000
  兜底;称重时间转 UTC 上送;错误三层分层沿用登录纵切
- health_record 域埋点强类型封装(06 §1.4:create_started/succeeded/
  failed + viewed,httpStatus 由业务码推导;viewed 按工单口径取列表曝光)
- health_record_display 纯函数(体重解析/展示、疫苗状态映射、42201
  规则前端拦截函数);PetsController 暴露 repository(22 号 §7 交接)
- 测试 177 → 205(+28)全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 13:17:17 +08:00
parent 97a1f46e3f
commit 5b34fa336b
10 changed files with 1528 additions and 0 deletions
@@ -0,0 +1,125 @@
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
/// health_record 域埋点强类型封装(06 号规划 §1.4 字典 v2;后端白名单
/// 已扩充就绪,24 号报告 §2.2)。沿用 13 号规范 §3.1 惯例:枚举编译期
/// 锁死,业务代码禁止手拼事件名与属性。
///
/// T2-13 挂接创建漏斗三事件 + viewededit/deleted 事件的挂接随
/// 编辑/删除交互落地(「标记完成」等)另行接线,见 25 号报告遗留。
/// 记录类型(06 §1.4 recordType 枚举,四类记录接口对应)。
enum HealthRecordType {
weight('weight'),
vaccine('vaccine'),
healthEvent('health_event'),
reminder('reminder');
const HealthRecordType(this.value);
final String value;
}
/// 记录创建入口(06 §1.4 entryPoint 枚举)。
enum HealthRecordEntryPoint {
petDetail('pet_detail'),
recordList('record_list'),
reminder('reminder');
const HealthRecordEntryPoint(this.value);
final String value;
}
/// 创建失败原因(06 §1.4 基底 + M2 验收新增三值)。与 pet 域同款
/// 网络归并口径:断网/超时/5xx 均并入 network_errorserver_error
/// 保留给无法归类的兜底。
enum HealthRecordFailureReason {
validationError('validation_error'),
permissionDenied('permission_denied'),
notFound('not_found'),
rateLimited('rate_limited'),
networkError('network_error'),
serverError('server_error');
const HealthRecordFailureReason(this.value);
final String value;
}
/// viewed 的来源(06 §1.4 source 枚举)。
enum HealthRecordViewSource {
recordList('record_list'),
petDetail('pet_detail'),
reminder('reminder');
const HealthRecordViewSource(this.value);
final String value;
}
class HealthRecordAnalytics {
HealthRecordAnalytics(this._track);
/// 生产传 `AnalyticsService.trackEvent`,测试传录制桩。
final TrackEventFn _track;
/// 进入某类记录的创建表单并产生首次输入(每次进入记一次,表单层去重)。
void createStarted({
required HealthRecordType recordType,
required HealthRecordEntryPoint entryPoint,
}) {
_track('health_record_create_started', {
'recordType': recordType.value,
'entryPoint': entryPoint.value,
});
}
/// 创建接口成功响应(漏斗事件,北极星与 H1/H3/H4 的核心数据源)。
///
/// [photoCount] 无照片为 0(M2 不做媒体上传,恒 0,字段随字典保留)。
void createSucceeded({
required HealthRecordType recordType,
required int durationMs,
int photoCount = 0,
}) {
_track('health_record_create_succeeded', {
'recordType': recordType.value,
'durationMs': durationMs,
'photoCount': photoCount,
});
}
/// 创建失败:失败响应 / 超时 / 本地校验拦截。
///
/// [errorCode] 为业务错误码(本地校验/网络错误时缺席);
/// [httpStatus] 由五位业务码推导(`code ~/ 100`);
/// [attemptSeq] 为本次表单会话内第几次提交尝试(从 1 起)。
void createFailed({
required HealthRecordType recordType,
required HealthRecordFailureReason reason,
required int attemptSeq,
int? errorCode,
}) {
_track('health_record_create_failed', {
'recordType': recordType.value,
'failureReason': reason.value,
'attemptSeq': attemptSeq,
'errorCode': ?errorCode,
if (errorCode != null && errorCode >= 10000)
'httpStatus': errorCode ~/ 100,
});
}
/// 记录曝光。06 §1.4 定义在详情页可见;M2 体重/疫苗无独立详情页,
/// 以「列表页每次进入首个成功加载」为曝光时点(每次进入一次、
/// 不随滚动逐条上报,防事件洪水的意图不变)。
void viewed({
required HealthRecordType recordType,
required HealthRecordViewSource source,
}) {
_track('health_record_viewed', {
'recordType': recordType.value,
'source': source.value,
});
}
}