fix: 埋点上传链路三处修复 + 注册手机号 +86 前缀
CI / flutter-gates (push) Successful in 1m3s

- events 上传地址接错:AnalyticsService 误用 auth(8081),实际端点在
  user 服务(8082),新增 patbondUserApiBaseUrl 并接线
- 冲刷时机:新增 flushNow(),SessionTracker 首次离开前台触发,
  修复低活跃用户凑不满 20 条事件永不上传
- 毒丸批次:4xx 永久性拒绝不再重回队列无限重试,丢弃并打日志
- Web/桌面 Platform API 降级(kIsWeb + _platformName)
- 注册手机号 UI 固定 +86 前缀、提交拼 E.164;AppTextField 支持 prefixText

51 测试全绿、analyze 0 问题;Linux 桌面实测注册成功,
curl 实测后端 /api/v1/events 校验行为符合契约。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 16:48:36 +08:00
parent 8ea6265046
commit 1afec6aad1
6 changed files with 53 additions and 4 deletions
+29 -1
View File
@@ -58,6 +58,21 @@ class AnalyticsService {
return '${Platform.operatingSystem}-${major ?? 'unknown'}';
}
/// 平台标识。契约枚举为 android/ios;Web/桌面为开发调试形态,
/// 上报值不在枚举内会被服务端逐条 rejected(不影响客户端),属预期。
static String _platformName() {
if (kIsWeb) {
return 'web';
}
if (Platform.isAndroid) {
return 'android';
}
if (Platform.isIOS) {
return 'ios';
}
return Platform.operatingSystem;
}
/// Sets userId after login/restore.
void identify(String userId) {
_userId = userId;
@@ -89,7 +104,7 @@ class AnalyticsService {
'sessionId': getSessionId(),
'clientTs': DateTime.now().toUtc().toIso8601String(),
'appVersion': _appVersion,
'platform': Platform.isAndroid ? 'android' : 'ios',
'platform': _platformName(),
'osVersion': _osVersion,
if (props != null && props.isNotEmpty) 'props': props,
};
@@ -103,6 +118,10 @@ class AnalyticsService {
}
}
/// 立即冲刷队列(退后台/会话切换时调用,避免低活跃用户凑不满
/// [_flushThreshold] 条导致事件永不上传)。
Future<void> flushNow() => _flush();
Future<void> _flush() async {
if (_flushing || _pendingEvents.isEmpty) return;
_flushing = true;
@@ -137,6 +156,15 @@ class AnalyticsService {
request.add(utf8.encode(jsonEncode({'events': events})));
final response = await request.close();
if (response.statusCode >= 400 && response.statusCode < 500) {
// 4xx 为永久性拒绝(校验失败/批量超限等),重试不可能成功;
// 丢弃并打日志,避免毒丸批次无限重回队列阻塞后续事件。
debugPrint(
'Analytics batch permanently rejected '
'(${response.statusCode}), dropping ${events.length} events',
);
return;
}
if (response.statusCode != 202) {
throw Exception('Upload failed with ${response.statusCode}');
}
+5
View File
@@ -14,6 +14,7 @@ import 'package:uuid/uuid.dart';
class SessionTracker with WidgetsBindingObserver {
SessionTracker({
this.timeout = const Duration(minutes: 30),
this.onLeaveForeground,
DateTime Function()? now,
}) : _now = now ?? DateTime.now,
_sessionId = const Uuid().v7();
@@ -21,6 +22,9 @@ class SessionTracker with WidgetsBindingObserver {
/// 后台超时阈值;构造参数化便于测试注入。
final Duration timeout;
/// 首次离开前台时回调(app 装配层用于触发埋点队列冲刷)。
final VoidCallback? onLeaveForeground;
/// 时钟注入口,测试免真实等待。
final DateTime Function() _now;
@@ -42,6 +46,7 @@ class SessionTracker with WidgetsBindingObserver {
} else if (_lastState == AppLifecycleState.resumed) {
// 首次离开前台才记时;inactive/hidden/paused 级联不覆盖。
_leftForegroundAt = _now();
onLeaveForeground?.call();
}
_lastState = state;
}