- 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:
@@ -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}');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+4
-2
@@ -43,11 +43,13 @@ class _AppState extends State<App> {
|
||||
widget.sessionManager ??
|
||||
SessionManager(store: const SecureTokenStore());
|
||||
|
||||
_sessionTracker = SessionTracker();
|
||||
_sessionTracker = SessionTracker(
|
||||
onLeaveForeground: () => _analytics.flushNow(),
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_sessionTracker);
|
||||
|
||||
_analytics = AnalyticsService(
|
||||
apiBaseUrl: patbondApiBaseUrl,
|
||||
apiBaseUrl: patbondUserApiBaseUrl,
|
||||
getAccessToken: () => sessionManager.accessToken,
|
||||
getSessionId: () => _sessionTracker.sessionId,
|
||||
);
|
||||
|
||||
@@ -10,6 +10,13 @@ const String patbondApiBaseUrl = String.fromEnvironment(
|
||||
defaultValue: 'http://127.0.0.1:8081',
|
||||
);
|
||||
|
||||
/// user 服务基地址(/api/v1/me、/api/v1/events):MVP 阶段 auth 与 user
|
||||
/// 分端口直连(ADR-002 无网关),`--dart-define=PATBOND_USER_API_BASE_URL=...` 注入。
|
||||
const String patbondUserApiBaseUrl = String.fromEnvironment(
|
||||
'PATBOND_USER_API_BASE_URL',
|
||||
defaultValue: 'http://127.0.0.1:8082',
|
||||
);
|
||||
|
||||
/// 构建全局共用的 Dio 实例。
|
||||
///
|
||||
/// `validateStatus` 放行所有状态码:错误信封由 [ApiClient] 统一解析成
|
||||
|
||||
@@ -11,6 +11,7 @@ class AppTextField extends StatefulWidget {
|
||||
super.key,
|
||||
this.controller,
|
||||
this.prefixIcon,
|
||||
this.prefixText,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.enabled = true,
|
||||
@@ -25,6 +26,10 @@ class AppTextField extends StatefulWidget {
|
||||
final String label;
|
||||
final TextEditingController? controller;
|
||||
final IconData? prefixIcon;
|
||||
|
||||
/// 固定前缀文本(如手机号的 '+86 '),仅展示与拼接语义,不进输入值。
|
||||
final String? prefixText;
|
||||
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final bool enabled;
|
||||
@@ -62,6 +67,7 @@ class _AppTextFieldState extends State<AppTextField> {
|
||||
labelText: widget.label,
|
||||
errorText: widget.errorText,
|
||||
helperText: widget.helperText,
|
||||
prefixText: widget.prefixText,
|
||||
prefixIcon: widget.prefixIcon == null
|
||||
? null
|
||||
: Icon(widget.prefixIcon, color: AppColors.muted),
|
||||
|
||||
@@ -147,7 +147,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
try {
|
||||
await widget.authRepository.register(
|
||||
username: _usernameCtrl.text.trim(),
|
||||
phone: _phoneCtrl.text.trim(),
|
||||
phone: '+86${_phoneCtrl.text.trim()}',
|
||||
password: _passwordCtrl.text,
|
||||
);
|
||||
TextInput.finishAutofillContext();
|
||||
@@ -232,6 +232,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
label: '手机号',
|
||||
controller: _phoneCtrl,
|
||||
prefixIcon: Icons.phone_iphone_rounded,
|
||||
prefixText: '+86 ',
|
||||
errorText: _phoneError,
|
||||
enabled: !_submitting,
|
||||
keyboardType: TextInputType.phone,
|
||||
|
||||
Reference in New Issue
Block a user