petsHero-AI/lib/core/auth/auth_service.dart
2026-03-13 22:04:57 +08:00

231 lines
7.9 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 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../adjust/adjust_events.dart';
import '../api/api_client.dart';
import '../api/api_config.dart';
import '../api/proxy_client.dart';
import '../api/services/user_api.dart';
import '../log/app_logger.dart';
import '../referrer/referrer_service.dart';
import '../user/user_state.dart';
/// 认证服务APP 启动时执行快速登录
class AuthService {
AuthService._();
static final _log = AppLogger('AuthService');
static Future<void>? _loginFuture;
/// 登录完成后的 Future需鉴权接口应 await 此 Future 再请求
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
static void _logMsg(String msg) {
_log.d(msg);
}
/// 获取设备 IDAndroid: androidId, iOS: identifierForVendor, Web: fallback
static Future<String> _getDeviceId() async {
final deviceInfo = DeviceInfoPlugin();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
final android = await deviceInfo.androidInfo;
return android.id;
case TargetPlatform.iOS:
final ios = await deviceInfo.iosInfo;
return ios.identifierForVendor ?? 'ios-unknown';
default:
return 'device-${DateTime.now().millisecondsSinceEpoch}';
}
}
/// 计算 signMD5(deviceId) 大写 32 位
static String _computeSign(String deviceId) {
final bytes = utf8.encode(deviceId);
final digest = md5.convert(bytes);
return digest.toString().toUpperCase();
}
/// 将 common_info 响应保存到全局,并解析 surge 中的 lucky是否开启三方支付
static void _saveCommonInfoToState(Map<String, dynamic> data) {
final reveal = data['reveal'] as int?;
if (reveal != null) UserState.setCredits(reveal);
final realm = data['realm'] as String?;
if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm);
final terminal = data['terminal'] as String?;
if (terminal != null && terminal.isNotEmpty) UserState.setUserName(terminal);
final navigate = data['navigate'] as String?;
if (navigate != null) UserState.setNavigate(navigate);
final surgeStr = data['surge'] as String?;
if (surgeStr != null && surgeStr.isNotEmpty) {
try {
final surge = json.decode(surgeStr) as Map<String, dynamic>?;
if (surge != null) {
final enable = surge['lucky'] as bool?;
UserState.setEnableThirdPartyPayment(enable);
// extConfigneed_wait = 是否展示 Video 菜单items = 图片列表(见 docs/extConfig.md
final needWait = surge['need_wait'] as bool?;
final items = surge['items'] as List<dynamic>?;
UserState.setExtConfig(
needShowVideoMenuValue: needWait,
items: items,
);
}
} catch (e) {
_logMsg('surge JSON 解析失败: $e');
}
}
}
/// APP 启动时调用快速登录
/// 启动时网络可能未就绪,会延迟后重试
static Future<void> init() async {
if (_loginFuture != null) return _loginFuture!;
final completer = Completer<void>();
_loginFuture = completer.future;
_logMsg('init: 开始快速登录');
const maxRetries = 3;
const retryDelay = Duration(seconds: 2);
try {
// 等待网络就绪(浏览器能访问但 App 报错时,多为启动时网络未初始化)
await Future<void>.delayed(const Duration(seconds: 2));
final deviceId = await _getDeviceId();
_logMsg('init: deviceId=$deviceId');
final sign = _computeSign(deviceId);
_logMsg('init: sign=$sign');
final crest = await ReferrerService.getReferrer();
if (crest != null && crest.isNotEmpty) {
_logMsg('init: crest(referrer)=$crest');
}
ApiResponse? res;
for (var i = 0; i < maxRetries; i++) {
if (i > 0) {
_logMsg('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...');
await Future<void>.delayed(retryDelay);
}
try {
res = await UserApi.fastLogin(
origin: deviceId,
resolution: sign,
digest: crest ?? '',
crest: crest,
);
break;
} catch (e) {
_logMsg('init: 第 ${i + 1} 次请求失败: $e');
if (i == maxRetries - 1) rethrow;
}
}
if (res == null) return;
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}');
if (res.isSuccess && res.data != null) {
final data = res.data as Map<String, dynamic>?;
final token = data?['reevaluate'] as String?;
if (token != null && token.isNotEmpty) {
ApiClient.instance.setUserToken(token);
_logMsg('init: 已设置 userToken');
} else {
_logMsg('init: 响应中无 reevaluate (userToken)');
}
final prefs = await SharedPreferences.getInstance();
final hadLoggedIn = prefs.getBool('adjust_has_logged_in') ?? false;
if (!hadLoggedIn) {
AdjustEvents.trackRegister();
await prefs.setBool('adjust_has_logged_in', true);
await prefs.setString(
'adjust_register_date',
DateTime.now().toIso8601String().substring(0, 10),
);
_logMsg('init: 首次登录,已上报 register');
}
final credits = data?['reveal'] as int?;
if (credits != null) {
UserState.setCredits(credits);
_logMsg('init: 已同步积分 $credits');
}
final uid = data?['asset'] as String?;
if (uid != null && uid.isNotEmpty) {
UserState.setUserId(uid);
_logMsg('init: 已设置 userId');
}
final avatarUrl = data?['realm'] as String?;
if (avatarUrl != null && avatarUrl.isNotEmpty) {
UserState.setAvatar(avatarUrl);
}
final name = data?['terminal'] as String?;
if (name != null && name.isNotEmpty) {
UserState.setUserName(name);
}
final countryCode = data?['navigate'] as String?;
if (countryCode != null) {
UserState.setNavigate(countryCode);
}
// 3. 归因上报digest 从 Adjust 取,暂无则用 install referrer
try {
final referrerRes = await UserApi.referrer(
sentinel: ApiConfig.appId,
asset: uid!,
digest: crest ?? '',
origin: deviceId,
accolade: 'android_adjust',
);
if (referrerRes.isSuccess) {
_logMsg('referrer 上报成功');
} else {
_logMsg(
'referrer 上报失败: code=${referrerRes.code} msg=${referrerRes.msg}');
}
} catch (e) {
_logMsg('referrer 请求异常: $e');
}
// 4. 获取用户通用信息,保存到全局并解析 surge
try {
final commonRes = await UserApi.getCommonInfo(
sentinel: ApiConfig.appId,
asset: uid,
);
if (commonRes.isSuccess && commonRes.data != null) {
final commonData = commonRes.data as Map<String, dynamic>?;
if (commonData != null) {
_saveCommonInfoToState(commonData);
_logMsg('common_info 已保存到全局');
}
_logMsg('common_info 响应:');
logWithEmbeddedJson(json.encode(commonRes.data));
} else {
_logMsg(
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}');
}
} catch (e) {
_logMsg('common_info 请求异常: $e');
}
} else {
_logMsg('init: 登录失败');
}
} catch (e, st) {
_logMsg('init: 异常 $e');
_logMsg('init: 堆栈 $st');
} finally {
if (!completer.isCompleted) completer.complete();
}
}
}