import 'dart:async'; import 'dart:math'; import 'package:patbond_flutter/features/community/feed_analytics.dart'; /// 一个「Feed 浏览段」的聚合器(06 号规划 §1.2 裁定的客户端实现)。 /// /// 段生命周期由页面驱动:进入 Feed(Tab 激活且分段在 Feed)开段; /// 离开(切 Tab / 切分段 / 退后台 / 页面销毁)时 [settle] 结算并经 /// [FeedAnalytics.feedViewed] 发**一条**聚合事件。 /// /// - 曝光判定:卡片可见面积 ≥ [visibleThreshold] 且持续 ≥ [dwell], /// 段内按 postId 去重;postId 只作内存去重键、绝不上报(隐私红线 2), /// 段结束即弃。 /// - durationMs 为前台时长(退后台即结算,段天然前台连续),上限 /// 截断 30 分钟防挂机污染(06 §1.4 实现注意)。 class FeedViewSegment { FeedViewSegment({DateTime Function()? now}) : _now = now ?? DateTime.now, _settled = false { _startedAt = _now(); } static const visibleThreshold = 0.5; static const dwell = Duration(milliseconds: 500); static const maxDuration = Duration(minutes: 30); final DateTime Function() _now; late final DateTime _startedAt; final Set _impressed = {}; final Map _dwellTimers = {}; int _loadMoreCount = 0; int _refreshCount = 0; bool _settled; /// 段内曝光卡片数(已去重;仅测试与结算读取)。 int get impressionCount => _impressed.length; /// 可见性回报:≥50% 起 500ms 驻留计时,跌破或滚出视口即取消; /// 驻留满即记曝光(去重后不再计时)。 void updateVisibility(String postId, double visibleFraction) { if (_settled) return; if (visibleFraction >= visibleThreshold) { if (_impressed.contains(postId) || _dwellTimers.containsKey(postId)) { return; } _dwellTimers[postId] = Timer(dwell, () { _dwellTimers.remove(postId); _impressed.add(postId); }); } else { _dwellTimers.remove(postId)?.cancel(); } } /// 用户触发的下拉刷新 / 失败重试(首屏自动预取不计)。 void recordRefresh() { if (!_settled) _refreshCount += 1; } /// 触底翻页请求(含尾部失败重试)。 void recordLoadMore() { if (!_settled) _loadMoreCount += 1; } /// 结算:取消在途驻留计时并冻结计数;幂等(重复调用返回 null, /// 保证一段恰好一条 feed_viewed)。 FeedViewSummary? settle() { if (_settled) return null; _settled = true; for (final timer in _dwellTimers.values) { timer.cancel(); } _dwellTimers.clear(); final elapsed = _now().difference(_startedAt); return FeedViewSummary( durationMs: min(elapsed.inMilliseconds, maxDuration.inMilliseconds), impressionCount: _impressed.length, loadMoreCount: _loadMoreCount, refreshCount: _refreshCount, ); } } /// [FeedViewSegment.settle] 的结算结果(feed_viewed 专有属性)。 class FeedViewSummary { const FeedViewSummary({ required this.durationMs, required this.impressionCount, required this.loadMoreCount, required this.refreshCount, }); final int durationMs; final int impressionCount; final int loadMoreCount; final int refreshCount; /// 结算即上报的便捷口。 void report(FeedAnalytics analytics, {FeedTab feedTab = FeedTab.home}) { analytics.feedViewed( feedTab: feedTab, durationMs: durationMs, impressionCount: impressionCount, loadMoreCount: loadMoreCount, refreshCount: refreshCount, ); } }