Files
patbond-flutter/lib/features/pets/health_record_analytics.dart
T
lixi e186ba3da9 新增:健康事件时间线接入真实数据(T2-14 时间线半边)
- 时间线页:occurred_at DESC cursor 分页 + 月分组组头,四态齐备;
  六类事件经 RecordTypeDot 映射(增补喂养/洗护/测量三型,色族复用
  已审计色对),类型 TagPill 双通道
- 事件录入表单:六类类型选择器、金额以元录入/整数分传输(money.dart
  换算)、UTC 时间戳约定与体重表单一致
- 事件编辑页:顶层 PATCH 差量提交(title/notes/amountCents + version),
  40902 照 T2-12 模式自动经时间线检索取新 version 重提(保留输入)
- 档案页:月度花费卡接 summary.monthlyExpense(分→元展示)、tz 透传
  设备时区固定偏移(T2-13 遗留③)、健康时间线导航入口
- 埋点:health_record 域 create 三事件 + viewed(recordType=health_event)
  挂通;新增 edit_succeeded/failed 封装(failureReason 含 conflict)
- 测试 224 → 250 全绿(+26);analyze 0 问题;format 无 diff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 13:49:48 +08:00

159 lines
5.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
/// health_record 域埋点强类型封装(06 号规划 §1.4 字典 v2;后端白名单
/// 已扩充就绪,24 号报告 §2.2)。沿用 13 号规范 §3.1 惯例:枚举编译期
/// 锁死,业务代码禁止手拼事件名与属性。
///
/// T2-13 挂接创建漏斗三事件 + viewedT2-14 补挂 edit_succeeded/failed
/// (健康事件编辑、疫苗标记完成/取消)。`health_record_deleted` 因 M2
/// 契约无删除端点暂无挂接点,留待删除交互落地。
/// 记录类型(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 验收新增三值;`conflict`
/// 仅编辑链路会出现——40902 乐观锁/条件更新守卫落空)。与 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'),
conflict('conflict');
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,
});
}
/// 编辑保存成功响应(06 §1.4:编辑不设 started)。
///
/// [fieldCount] 为本次变更字段数(差量 PATCH 的键数,不含 version)。
void editSucceeded({
required HealthRecordType recordType,
required int fieldCount,
}) {
_track('health_record_edit_succeeded', {
'recordType': recordType.value,
'fieldCount': fieldCount,
});
}
/// 编辑保存失败(失败原因含 `conflict`——40902 版本/状态守卫冲突,
/// M2 验收「并发冲突明确」场景的数据面)。属性集无 attemptSeq
/// (白名单对齐 06 §1.5)。
void editFailed({
required HealthRecordType recordType,
required HealthRecordFailureReason reason,
int? errorCode,
}) {
_track('health_record_edit_failed', {
'recordType': recordType.value,
'failureReason': reason.value,
'errorCode': ?errorCode,
if (errorCode != null && errorCode >= 10000)
'httpStatus': errorCode ~/ 100,
});
}
}