client_framework/lib/src/services/auth_service.dart
2026-04-22 23:08:39 +08:00

531 lines
18 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 'dart:async';
import 'package:flutter/foundation.dart';
import '../api/api_client.dart';
import '../api/proxy_client.dart';
import '../config/attribution_config.dart';
import '../config/ext_config_models.dart';
import '../config/ext_config_runtime.dart';
import '../config/video_home_runtime.dart';
import '../entities/user_entities.dart';
import 'adjust_service.dart';
import 'analytics_attribution_callbacks.dart';
import 'facebook_service.dart';
import 'login_identity_cache.dart';
import 'user_api.dart';
/// [FrameworkAuthService.start] 中 `fast_login` 在拿不到 Play Install Referrer 时使用的 `referer` 兜底(自然安装)。
const String _fastLoginPlayReferrerFallback =
'utm_source=google-play&utm_medium=organic';
/// 认证服务回调
/// 用于在认证流程各阶段通知调用方
abstract class AuthServiceCallbacks {
/// 获取设备 ID由调用方实现
Future<String> getDeviceId();
/// 计算签名(由调用方实现)
String computeSign(String deviceId);
/// 登录成功后回调
void onLoginSuccess(FastLoginResponse data) {}
/// 通用信息获取后回调
void onCommonInfoLoaded(CommonInfoResponse data) {}
/// 登录失败回调
void onLoginFailed(String msg) {}
}
/// APP 启动认证服务
/// 封装完整的启动登录流程,包括:
/// 1. 快速登录
/// 2. 归因上报
/// 3. 获取通用信息
///
/// **换皮默认行为**(无需宿主接入):
/// - `fast_login``type` **强制**为 `gg`Google 归因);`referer` 优先 Google Play Install Referrer取不到则使用 [_fastLoginPlayReferrerFallback],不再切换 Adjust/Facebook 的 type。
/// - 登录成功且 `userId` 非空:将 `userId` 与本次 `deviceId` 写入 [LoginIdentityCache]。
/// - 登录失败(含无响应、异常、`code != 0`、或成功体无 `userId`):上报 Facebook 自定义事件 [facebookLoginFailedEventName]
/// 参数:`server_error_code`、`user_id`(响应中有则带,否则空串)、`device_id`(本次启动已解析的设备 ID若尚未取到则为空串
/// 未能取得用户 ID 时另带 `register_faild` = `register faild`。
/// - [UserApi.getCommonInfo] 失败、请求异常,或成功但响应体无 `extConfig` 字符串:上报 Facebook [facebookExtConfigFailedEventName]`user_id`、`device_id`、`server_error_code`、可选 `server_error_msg`)。
abstract class FrameworkAuthService {
static AuthServiceCallbacks? _callbacks;
static Future<void>? _loginFuture;
static bool _isInitialized = false;
/// 登录是否已完成
static final ValueNotifier<bool> isLoginComplete = ValueNotifier(false);
/// 最近一次快速登录成功时的 [FastLoginResponse.userId](失败或未登录为空)。
///
/// 宿主在调用需 `userId` 的接口(如生图 `create-task`)前可读取;若为空应提示用户稍后重试。
static String? lastLoggedInUserId;
/// 登录完成后的 Future需鉴权接口应 await 此 Future 再请求
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
/// 初始化认证服务。
///
/// 默认注册 [AnalyticsAttributionCallbacks](与 Adjust 缓存一致)。可传入
/// [attributionCallbacks] 覆盖(例如自定义 referer 格式)。
static void init(
AuthServiceCallbacks callbacks, {
AttributionCallbacks? attributionCallbacks,
}) {
AttributionService.init(
attributionCallbacks ?? AnalyticsAttributionCallbacks(),
);
_callbacks = callbacks;
_isInitialized = true;
}
/// 启动登录流程
/// [delaySeconds] 启动延迟秒数,默认 2 秒
/// [maxRetries] 最大重试次数,默认 3 次
/// [retryDelaySeconds] 重试延迟秒数,默认 2 秒
static Future<void> start({
int delaySeconds = 2,
int maxRetries = 3,
int retryDelaySeconds = 2,
}) async {
if (!_isInitialized || _callbacks == null) {
throw StateError(
'FrameworkAuthService not initialized. '
'Call FrameworkAuthService.init(callbacks) first.',
);
}
if (_loginFuture != null) return _loginFuture!;
final completer = Completer<void>();
_loginFuture = completer.future;
if (kDebugMode) {
debugPrint('[AuthService] start: 开始登录流程');
}
/// 供 [catch] 上报 Facebook 使用:若在 [getDeviceId] 成功前抛错则为空串。
var deviceIdForFacebookFailure = '';
try {
await Future<void>.delayed(Duration(seconds: delaySeconds));
final deviceId = await _callbacks!.getDeviceId();
deviceIdForFacebookFailure = deviceId;
if (kDebugMode) {
debugPrint('[AuthService] start: deviceId=$deviceId');
}
final sign = _callbacks!.computeSign(deviceId);
if (kDebugMode) {
debugPrint('[AuthService] start: sign=$sign');
}
// fast_login强制使用 Google 归因类型 `gg`referer 优先 Play Install Referrer无则自然安装 UTM 兜底。
final playReferrer = AdjustService.cachedPlayReferrer;
final fastLoginReferer = (playReferrer != null && playReferrer.isNotEmpty)
? playReferrer
: _fastLoginPlayReferrerFallback;
const fastLoginType = 'gg';
if (kDebugMode) {
debugPrint(
'[AuthService] start: fast_login type=$fastLoginType refererLen=${fastLoginReferer.length}',
);
}
// 尝试快速登录
EntityResponse<FastLoginResponse>? res;
for (var i = 0; i < maxRetries; i++) {
if (i > 0) {
if (kDebugMode) {
debugPrint('[AuthService] start: 第 ${i + 1} 次重试...');
}
await Future<void>.delayed(Duration(seconds: retryDelaySeconds));
}
try {
final cfg = ApiClient.instance.config;
final appType = defaultTargetPlatform == TargetPlatform.iOS
? cfg.backendAppTypeIOS
: cfg.backendAppTypeAndroid;
res = await UserApi.fastLogin(
deviceId: deviceId,
sign: sign,
referer: fastLoginReferer,
app: appType,
type: fastLoginType,
);
break;
} catch (e) {
if (kDebugMode) {
debugPrint('[AuthService] start: 第 ${i + 1} 次请求失败: $e');
}
if (i == maxRetries - 1) rethrow;
}
}
if (res == null) {
lastLoggedInUserId = null;
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
_logFacebookLoginFailed(
serverCode: -1,
missingUserId: true,
userIdFromServer: null,
deviceId: deviceId,
);
completer.complete();
return;
}
if (kDebugMode) {
debugPrint('[AuthService] start: 登录结果 code=${res.code} msg=${res.msg}');
}
if (res.isSuccess && res.data != null) {
final loginData = res.data!;
final uid = loginData.userId?.trim();
lastLoggedInUserId = uid != null && uid.isNotEmpty ? uid : null;
// 设置 Token
if (loginData.userToken != null && loginData.userToken!.isNotEmpty) {
ApiClient.instance.setUserToken(loginData.userToken!);
if (kDebugMode) {
debugPrint('[AuthService] start: 已设置 userToken');
}
}
if (uid != null && uid.isNotEmpty) {
unawaited(
LoginIdentityCache.writeUserAndDeviceId(
userId: uid,
deviceId: deviceId,
),
);
} else {
_logFacebookLoginFailed(
serverCode: res.code,
missingUserId: true,
userIdFromServer: loginData.userId,
deviceId: deviceId,
);
}
// 回调登录成功
_callbacks!.onLoginSuccess(loginData);
// 上报归因并获取通用信息
await _reportReferrersAndLoadCommonInfo(
uid: loginData.userId ?? '',
deviceId: deviceId,
);
} else {
lastLoggedInUserId = null;
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
_logFacebookLoginFailedFromResponse(res, deviceId: deviceId);
_callbacks!.onLoginFailed(res.msg);
}
} catch (e, st) {
lastLoggedInUserId = null;
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
if (kDebugMode) {
debugPrint('[AuthService] start: 异常 $e\n$st');
}
_logFacebookLoginFailed(
serverCode: -1,
missingUserId: true,
userIdFromServer: null,
deviceId: deviceIdForFacebookFailure,
);
_callbacks!.onLoginFailed(e.toString());
} finally {
if (!completer.isCompleted) {
completer.complete();
isLoginComplete.value = true;
}
}
}
/// 上报归因并获取通用信息
static Future<void> _reportReferrersAndLoadCommonInfo({
required String uid,
required String deviceId,
}) async {
if (uid.isEmpty) {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
if (kDebugMode) {
debugPrint(
'[AuthService] common_info: 跳过userId 为空),已标记 common_info 失败',
);
}
return;
}
final config = ApiClient.instance.config;
final backendApp = defaultTargetPlatform == TargetPlatform.iOS
? config.backendAppTypeIOS
: config.backendAppTypeAndroid;
// 上报 Adjust 归因
var adjustReferrerTried = false;
var adjustReferrerOk = false;
var adjustReferrerCode = 0;
var adjustReferrerMsg = '';
final adjustReferer = await AttributionService.getAdjustReferrer();
if (adjustReferer != null && adjustReferer.isNotEmpty) {
adjustReferrerTried = true;
final adjustType = defaultTargetPlatform == TargetPlatform.iOS
? 'ios_adjust'
: 'android_adjust';
try {
final rAdjust = await UserApi.referrer(
app: backendApp,
userId: uid,
referer: adjustReferer,
deviceId: deviceId,
type: adjustType,
);
if (rAdjust.isSuccess) {
adjustReferrerOk = true;
} else {
adjustReferrerCode = rAdjust.code;
adjustReferrerMsg = rAdjust.msg;
}
if (kDebugMode) {
debugPrint(
'[AuthService] referrer($adjustType): ${rAdjust.isSuccess ? "成功" : "失败"}');
}
} catch (e) {
adjustReferrerCode = -110;
adjustReferrerMsg = e.toString();
if (kDebugMode) {
debugPrint('[AuthService] referrer($adjustType): 异常 $e');
}
}
}
// 上报 Google Play 归因(从 AdjustService 获取缓存的 referrer
var ggReferrerTried = false;
var ggReferrerOk = false;
var ggReferrerCode = 0;
var ggReferrerMsg = '';
final playReferrer = AdjustService.cachedPlayReferrer;
if (playReferrer != null && playReferrer.isNotEmpty) {
ggReferrerTried = true;
try {
final rGg = await UserApi.referrer(
app: backendApp,
userId: uid,
referer: playReferrer,
deviceId: deviceId,
type: 'gg',
);
if (rGg.isSuccess) {
ggReferrerOk = true;
} else {
ggReferrerCode = rGg.code;
ggReferrerMsg = rGg.msg;
}
if (kDebugMode) {
debugPrint(
'[AuthService] referrer(gg): ${rGg.isSuccess ? "成功" : "失败"}');
}
} catch (e) {
ggReferrerCode = -110;
ggReferrerMsg = e.toString();
if (kDebugMode) {
debugPrint('[AuthService] referrer(gg): 异常 $e');
}
}
}
if (adjustReferrerTried &&
ggReferrerTried &&
!adjustReferrerOk &&
!ggReferrerOk) {
_logFacebookReferrerBothFailed(
userId: uid,
deviceId: deviceId,
adjustCode: adjustReferrerCode,
adjustMsg: adjustReferrerMsg,
ggCode: ggReferrerCode,
ggMsg: ggReferrerMsg,
);
}
// 获取通用信息
try {
final commonRes = await UserApi.getCommonInfo(
app: backendApp,
pkg: config.packageName,
userId: uid,
deviceId: deviceId,
);
if (commonRes.isSuccess && commonRes.data != null) {
final info = commonRes.data!;
final extRaw = info.extConfig?.trim();
final extConfigMissing = extRaw == null || extRaw.isEmpty;
ExtConfigRuntime.applyCommonInfoSuccess(info);
_callbacks?.onCommonInfoLoaded(info);
unawaited(
VideoHomeRuntime.hydrateAfterCommonInfo(
userId: uid,
app: backendApp,
),
);
if (kDebugMode) {
debugPrint('[AuthService] common_info: 获取成功');
}
if (extConfigMissing) {
_logFacebookExtConfigFailed(
userId: uid,
deviceId: deviceId,
serverCode: 0,
serverMsg: 'ext_config_missing',
);
}
} else {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
_logFacebookExtConfigFailed(
userId: uid,
deviceId: deviceId,
serverCode: commonRes.code,
serverMsg: commonRes.msg,
);
if (kDebugMode) {
debugPrint(
'[AuthService] common_info: 失败 code=${commonRes.code} msg=${commonRes.msg}');
}
}
} catch (e) {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
_logFacebookExtConfigFailed(
userId: uid,
deviceId: deviceId,
serverCode: -1,
serverMsg: e.toString(),
);
if (kDebugMode) {
debugPrint('[AuthService] common_info: 异常 $e');
}
}
}
/// 解析 extConfig JSON 字符串为 Map兼容旧代码结构化解析请用 [ExtConfigData.parse])。
static Map<String, dynamic>? parseExtConfig(String? extConfigStr) {
return ExtConfigData.parseRawMap(extConfigStr);
}
/// Facebook 自定义事件名(与产品约定一致:`LoginFaild`)。
static const String facebookLoginFailedEventName = 'LoginFaild';
/// 两次归因上报Adjust + `gg`)均请求且均失败时上报(与产品约定:`Referer_faild`)。
static const String facebookReferrerBothFailedEventName = 'Referer_faild';
/// `common_info` 失败或响应中无 `extConfig` 时上报(与产品约定:`ExtConfigFaild`)。
static const String facebookExtConfigFailedEventName = 'ExtConfigFaild';
static const int _facebookParamMaxLen = 500;
static String _truncateForFacebookParam(String s) {
final t = s.trim();
if (t.length <= _facebookParamMaxLen) return t;
return '${t.substring(0, _facebookParamMaxLen)}';
}
/// Adjust 与 Google Play 两条 [UserApi.referrer] 都发起且都未成功时调用(不阻塞、失败静默)。
static void _logFacebookReferrerBothFailed({
required String userId,
required String deviceId,
required int adjustCode,
required String adjustMsg,
required int ggCode,
required String ggMsg,
}) {
final parts = <String>[
if (adjustMsg.trim().isNotEmpty)
'adjust: ${_truncateForFacebookParam(adjustMsg)}',
if (ggMsg.trim().isNotEmpty) 'gg: ${_truncateForFacebookParam(ggMsg)}',
];
final combinedMsg = parts.join(' | ');
final params = <String, dynamic>{
'user_id': userId.trim(),
'device_id': deviceId.trim(),
'server_error_code': '$adjustCode/$ggCode',
if (combinedMsg.isNotEmpty) 'server_error_msg': combinedMsg,
};
FacebookService.logEvent(
facebookReferrerBothFailedEventName,
parameters: params,
);
}
/// [UserApi.getCommonInfo] 失败、异常,或成功但缺少 `extConfig` 时调用(不阻塞、失败静默)。
static void _logFacebookExtConfigFailed({
required String userId,
required String deviceId,
required int serverCode,
String serverMsg = '',
}) {
final msg = serverMsg.trim();
final params = <String, dynamic>{
'user_id': userId.trim(),
'device_id': deviceId.trim(),
'server_error_code': '$serverCode',
if (msg.isNotEmpty) 'server_error_msg': _truncateForFacebookParam(msg),
};
FacebookService.logEvent(
facebookExtConfigFailedEventName,
parameters: params,
);
}
static void _logFacebookLoginFailedFromResponse(
EntityResponse<FastLoginResponse> res, {
required String deviceId,
}) {
final uid = res.data?.userId?.trim();
final missingUserId = uid == null || uid.isEmpty;
_logFacebookLoginFailed(
serverCode: res.code,
missingUserId: missingUserId,
userIdFromServer: res.data?.userId,
deviceId: deviceId,
);
}
/// 登录失败或未拿到用户 ID 时上报 Meta / Facebook App Events不阻塞、失败静默
///
/// - [serverCode]:接口 [EntityResponse.code];无响应或异常时为 `-1`。
/// - [userIdFromServer]:解密后响应里的 `userId`;无则上报空串。
/// - [deviceId]:本次流程使用的设备 ID未取到时为空串。
/// - [missingUserId] 为 `true` 时附带参数 `register_faild` = `register faild`。
static void _logFacebookLoginFailed({
required int serverCode,
required bool missingUserId,
String? userIdFromServer,
required String deviceId,
}) {
final uid = userIdFromServer?.trim() ?? '';
final did = deviceId.trim();
final params = <String, dynamic>{
'server_error_code': '$serverCode',
'user_id': uid,
'device_id': did,
if (missingUserId) 'register_faild': 'register faild',
};
FacebookService.logEvent(
facebookLoginFailedEventName,
parameters: params,
);
}
}