From 982bed4802b86453ef63c0d6d70a3d6df9d316e0 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 22 Apr 2026 23:08:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=8C=E9=94=99=E8=AF=AF=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 6 +- docs/video_home_data_flow.md | 2 +- lib/client_proxy_framework.dart | 2 + lib/src/api/proxy_client.dart | 11 +- lib/src/entities/feedback_entities.dart | 16 ++ lib/src/entities/image_entities.dart | 16 ++ lib/src/services/auth_service.dart | 254 ++++++++++++++++-- .../image_presigned_upload_create_flow.dart | 4 + .../image_upload_expected_size_cache.dart | 44 +++ lib/src/services/login_identity_cache.dart | 43 +++ lib/src/services/user_api.dart | 8 +- 11 files changed, 370 insertions(+), 36 deletions(-) create mode 100644 lib/src/services/image_upload_expected_size_cache.dart create mode 100644 lib/src/services/login_identity_cache.dart diff --git a/docs/README.md b/docs/README.md index f45b2c8..18ebf92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -96,7 +96,7 @@ AES 加密 发送请求 ``` -**请求头**:`ProxyClient` 会将 [AppConfig.packageName] 写入映射后的包名字段(原始名 `pkg`);若已设置用户 token,默认还会写入 `User_token`。**`UserApi.fast_login` 等无需登录态接口**内部使用 `includeUserTokenInHeader: false`,避免把旧 token 打进 `filter_type`。其余请求也可在直接调用 `ProxyClient.request` 时传入该参数。 +**请求头**:`ProxyClient` 默认将 [AppConfig.packageName] 写入映射后的包名字段(原始名 `pkg`);若已设置用户 token,默认还会写入 `User_token`。**`UserApi.fastLogin`** 对内层请求使用 `includeUserTokenInHeader: false`(不传 token,`pkg` 仍按默认行为传递)。其余接口可按需在 `ProxyClient.request` 上传入上述开关。 **请求体**:各 `*Api` 方法使用与《客户端指南》解密表一致的**原始字段名**(如 `referer`、`deviceId`、`fileUrls` / `contentType` / `content`)。 @@ -146,9 +146,9 @@ final res = await UserApi.fastLogin( deviceId: '设备ID', sign: 'MD5(deviceId)大写', app: 'HAndroid', // 必填:HIOS / HAndroid - referer: '归因来源', // 可选 + referer: '归因来源', // 可选;`gg` 时常为 Play Install Referrer;框架 start 取不到时用 utm_source=google-play&utm_medium=organic ch: '渠道号', // 可选 - type: 'fb', // 可选;未传时默认 fb + type: 'gg', // 可选;未传时默认 gg(Google Play 归因) ); if (res.isSuccess) { diff --git a/docs/video_home_data_flow.md b/docs/video_home_data_flow.md index 364bf87..b98c909 100644 --- a/docs/video_home_data_flow.md +++ b/docs/video_home_data_flow.md @@ -49,7 +49,7 @@ sequenceDiagram ### 3.1 启动与快速登录 - 延迟与重试策略由 `FrameworkAuthService.start` 控制(默认启动延迟、登录重试等)。 -- **`UserApi.fastLogin`**:`POST /v1/user/fast_login`。请求头**仅**带 `pkg`,**不带** `User_token`(见 `UserApi` 文档注释)。 +- **`UserApi.fastLogin`**:`POST /v1/user/fast_login`。线网内层 headers 保留 `pkg`,但**不注入** `User_token`(见 `UserApi` / `ProxyClient` 注释)。Query `type` 未传时默认为 **`gg`**(Google Play 归因)。`FrameworkAuthService` 强制 `type=gg`:`referer` 优先 Play Install Referrer,取不到时用 `utm_source=google-play&utm_medium=organic`。 - 成功后框架将返回的 `userToken` 写入 **`ApiClient.instance.setUserToken`**,此后代理请求自动附带 `pkg` 与 `User_token`(见 `ProxyClient` 行为与 `UserApi` 说明)。 ### 3.2 归因上报(与首页数据并行准备) diff --git a/lib/client_proxy_framework.dart b/lib/client_proxy_framework.dart index 9a1f8d5..9883a6d 100644 --- a/lib/client_proxy_framework.dart +++ b/lib/client_proxy_framework.dart @@ -32,12 +32,14 @@ export 'src/services/analytics_attribution_callbacks.dart'; export 'src/services/analytics_events.dart'; export 'src/services/analytics_service.dart'; export 'src/services/auth_service.dart'; +export 'src/services/login_identity_cache.dart'; export 'src/services/facebook_service.dart'; export 'src/services/feedback_api.dart'; export 'src/services/image_api.dart'; export 'src/services/image_progress_poll.dart'; export 'src/services/image_compress.dart'; export 'src/services/image_presigned_upload_create_flow.dart'; +export 'src/services/image_upload_expected_size_cache.dart'; export 'src/services/image_task_history.dart'; export 'src/services/task_upload_cover_store.dart'; export 'src/services/user_account_refresh.dart'; diff --git a/lib/src/api/proxy_client.dart b/lib/src/api/proxy_client.dart index 53a0fdf..ea57ad5 100644 --- a/lib/src/api/proxy_client.dart +++ b/lib/src/api/proxy_client.dart @@ -104,11 +104,11 @@ class ProxyClient { /// 响应 data 会自动从线网转回逻辑字段名。 /// /// **请求头(自动注入)** - /// - [AppConfig.packageName] → 逻辑字段名 `pkg`(再映射为线网请求头键) + /// - 若 [includePackageInHeader] 为 true(默认):[AppConfig.packageName] → 逻辑字段名 `pkg`(再映射为线网请求头键) /// - 若已 [userToken] 且 [includeUserTokenInHeader] 为 true,则注入 `User_token` /// /// 与文档一致:**设备快速登录等无需登录态接口**应传 `includeUserTokenInHeader: false`, - /// 避免历史 token 进入 `filter_type`。 + /// 避免历史 token 进入 `filter_type`。若同时 `includePackageInHeader: false`,则内层 headers 不注入 `pkg`(由 query 等自行携带包名)。 Future request({ required String path, required String method, @@ -116,12 +116,13 @@ class ProxyClient { Map? queryParams, Map? body, bool includeUserTokenInHeader = true, + bool includePackageInHeader = true, }) async { final pk = config.proxyKeys; final mapping = config.fieldMapping; var headersMap = Map.from(headers ?? {}); - if (config.packageName.isNotEmpty) { + if (includePackageInHeader && config.packageName.isNotEmpty) { headersMap[mapping.headerPackageNameField] = config.packageName; } if (includeUserTokenInHeader && @@ -179,7 +180,7 @@ class ProxyClient { /// [headers]、[queryParams]、[body] 使用**业务逻辑字段名**。 /// [entityFactory] 用于将映射后的 data 转换为实体对象。 /// - /// 参见 [request] 的 [includeUserTokenInHeader] 说明。 + /// 参见 [request] 的 [includeUserTokenInHeader]、[includePackageInHeader] 说明。 Future> requestEntity({ required String path, required String method, @@ -188,6 +189,7 @@ class ProxyClient { Map? queryParams, Map? body, bool includeUserTokenInHeader = true, + bool includePackageInHeader = true, }) async { final response = await request( path: path, @@ -196,6 +198,7 @@ class ProxyClient { queryParams: queryParams, body: body, includeUserTokenInHeader: includeUserTokenInHeader, + includePackageInHeader: includePackageInHeader, ); if (response.isSuccess) { diff --git a/lib/src/entities/feedback_entities.dart b/lib/src/entities/feedback_entities.dart index b3a1e2c..3efa278 100644 --- a/lib/src/entities/feedback_entities.dart +++ b/lib/src/entities/feedback_entities.dart @@ -8,6 +8,7 @@ class FeedbackUploadPresignedUrlResponse extends Entity { this.uploadUrl, this.filePath, this.putHeaders, + this.expectedSize, }); final String? uploadUrl; @@ -16,6 +17,9 @@ class FeedbackUploadPresignedUrlResponse extends Entity { /// 与 [UploadPresignedUrlResponse.putHeaders] 一致:PUT 到对象存储时的额外头。 final Map? putHeaders; + /// 服务端返回的上传大小上限(字节),逻辑字段名 `expectedSize`;未下发时为 `null`。 + final int? expectedSize; + static String _headerValueToString(dynamic v) { if (v == null) return ''; if (v is String) return v; @@ -57,6 +61,14 @@ class FeedbackUploadPresignedUrlResponse extends Entity { return out.isEmpty ? null : out; } + static int? _readIntField(Map json, String key) { + final v = json[key]; + if (v == null) return null; + if (v is int) return v; + if (v is num) return v.toInt(); + return int.tryParse(v.toString().trim()); + } + @override factory FeedbackUploadPresignedUrlResponse.fromJson( Map json) { @@ -64,6 +76,9 @@ class FeedbackUploadPresignedUrlResponse extends Entity { uploadUrl: json['uploadUrl'] as String?, filePath: json['filePath'] as String?, putHeaders: _parsePutHeaders(json), + expectedSize: _readIntField(json, 'expectedSize') ?? + _readIntField(json, 'maxFileSize') ?? + _readIntField(json, 'maxSize'), ); } @@ -72,6 +87,7 @@ class FeedbackUploadPresignedUrlResponse extends Entity { 'uploadUrl': uploadUrl, 'filePath': filePath, if (putHeaders != null) 'putHeaders': putHeaders, + if (expectedSize != null) 'expectedSize': expectedSize, }; } diff --git a/lib/src/entities/image_entities.dart b/lib/src/entities/image_entities.dart index f2a99e8..bc3b688 100644 --- a/lib/src/entities/image_entities.dart +++ b/lib/src/entities/image_entities.dart @@ -405,6 +405,7 @@ class UploadPresignedUrlResponse extends Entity { this.uploadUrl, this.filePath, this.putHeaders, + this.expectedSize, }); final String? uploadUrl; @@ -413,6 +414,9 @@ class UploadPresignedUrlResponse extends Entity { /// 上传到对象存储时额外请求头(如服务端返回的签名头;解密后为 business 字段名)。 final Map? putHeaders; + /// 服务端返回的单次上传大小上限(字节),逻辑字段名 `expectedSize`;未下发时为 `null`。 + final int? expectedSize; + /// 将任意 JSON 头值压成 [http] 要求的 [String](避免 `TypeError`)。 static String _headerValueToString(dynamic v) { if (v == null) return ''; @@ -462,6 +466,14 @@ class UploadPresignedUrlResponse extends Entity { return v.toString(); } + static int? _readIntField(Map json, String key) { + final v = json[key]; + if (v == null) return null; + if (v is int) return v; + if (v is num) return v.toInt(); + return int.tryParse(v.toString().trim()); + } + @override factory UploadPresignedUrlResponse.fromJson(Map json) { // FunyMee 等换皮:`uploadUrl1`/`filePath1`(文档 wire:harden / generate)。 @@ -476,6 +488,9 @@ class UploadPresignedUrlResponse extends Entity { uploadUrl: upload, filePath: path, putHeaders: _parsePutHeaders(json), + expectedSize: _readIntField(json, 'expectedSize') ?? + _readIntField(json, 'maxFileSize') ?? + _readIntField(json, 'maxSize'), ); } @@ -484,6 +499,7 @@ class UploadPresignedUrlResponse extends Entity { 'uploadUrl': uploadUrl, 'filePath': filePath, if (putHeaders != null) 'putHeaders': putHeaders, + if (expectedSize != null) 'expectedSize': expectedSize, }; } diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index ed53374..8448230 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -11,8 +11,14 @@ 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 { @@ -37,6 +43,14 @@ abstract class AuthServiceCallbacks { /// 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? _loginFuture; @@ -91,10 +105,14 @@ abstract class FrameworkAuthService { debugPrint('[AuthService] start: 开始登录流程'); } + /// 供 [catch] 上报 Facebook 使用:若在 [getDeviceId] 成功前抛错则为空串。 + var deviceIdForFacebookFailure = ''; + try { await Future.delayed(Duration(seconds: delaySeconds)); final deviceId = await _callbacks!.getDeviceId(); + deviceIdForFacebookFailure = deviceId; if (kDebugMode) { debugPrint('[AuthService] start: deviceId=$deviceId'); } @@ -104,26 +122,17 @@ abstract class FrameworkAuthService { debugPrint('[AuthService] start: sign=$sign'); } - final referer = await AttributionService.getReferrer(); - if (kDebugMode && referer != null) { - debugPrint('[AuthService] start: referer=$referer'); - } - - // 确定归因类型 - String? referrerType; - final adjustReferrer = await AttributionService.getAdjustReferrer(); - final fbReferrer = await AttributionService.getFacebookReferrer(); - - if (adjustReferrer != null && adjustReferrer.isNotEmpty) { - referrerType = defaultTargetPlatform == TargetPlatform.iOS - ? 'ios_adjust' - : 'android_adjust'; - } else if (fbReferrer != null && fbReferrer.isNotEmpty) { - referrerType = 'fb'; - } + // 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: referrerType=$referrerType'); + debugPrint( + '[AuthService] start: fast_login type=$fastLoginType refererLen=${fastLoginReferer.length}', + ); } // 尝试快速登录 @@ -143,9 +152,9 @@ abstract class FrameworkAuthService { res = await UserApi.fastLogin( deviceId: deviceId, sign: sign, - referer: referer ?? '', + referer: fastLoginReferer, app: appType, - type: referrerType, + type: fastLoginType, ); break; } catch (e) { @@ -160,6 +169,12 @@ abstract class FrameworkAuthService { lastLoggedInUserId = null; VideoHomeRuntime.reset(); ExtConfigRuntime.applyCommonInfoFailure(); + _logFacebookLoginFailed( + serverCode: -1, + missingUserId: true, + userIdFromServer: null, + deviceId: deviceId, + ); completer.complete(); return; } @@ -171,8 +186,7 @@ abstract class FrameworkAuthService { if (res.isSuccess && res.data != null) { final loginData = res.data!; final uid = loginData.userId?.trim(); - lastLoggedInUserId = - uid != null && uid.isNotEmpty ? uid : null; + lastLoggedInUserId = uid != null && uid.isNotEmpty ? uid : null; // 设置 Token if (loginData.userToken != null && loginData.userToken!.isNotEmpty) { @@ -182,6 +196,22 @@ abstract class FrameworkAuthService { } } + 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); @@ -194,6 +224,7 @@ abstract class FrameworkAuthService { lastLoggedInUserId = null; VideoHomeRuntime.reset(); ExtConfigRuntime.applyCommonInfoFailure(); + _logFacebookLoginFailedFromResponse(res, deviceId: deviceId); _callbacks!.onLoginFailed(res.msg); } } catch (e, st) { @@ -203,6 +234,12 @@ abstract class FrameworkAuthService { 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) { @@ -234,8 +271,14 @@ abstract class FrameworkAuthService { : 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'; @@ -247,11 +290,19 @@ abstract class FrameworkAuthService { 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'); } @@ -259,8 +310,14 @@ abstract class FrameworkAuthService { } // 上报 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, @@ -269,17 +326,39 @@ abstract class FrameworkAuthService { 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( @@ -289,8 +368,11 @@ abstract class FrameworkAuthService { deviceId: deviceId, ); if (commonRes.isSuccess && commonRes.data != null) { - ExtConfigRuntime.applyCommonInfoSuccess(commonRes.data!); - _callbacks?.onCommonInfoLoaded(commonRes.data!); + 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, @@ -300,9 +382,23 @@ abstract class FrameworkAuthService { 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}'); @@ -311,6 +407,12 @@ abstract class FrameworkAuthService { } catch (e) { VideoHomeRuntime.reset(); ExtConfigRuntime.applyCommonInfoFailure(); + _logFacebookExtConfigFailed( + userId: uid, + deviceId: deviceId, + serverCode: -1, + serverMsg: e.toString(), + ); if (kDebugMode) { debugPrint('[AuthService] common_info: 异常 $e'); } @@ -321,4 +423,108 @@ abstract class FrameworkAuthService { static Map? 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 = [ + if (adjustMsg.trim().isNotEmpty) + 'adjust: ${_truncateForFacebookParam(adjustMsg)}', + if (ggMsg.trim().isNotEmpty) 'gg: ${_truncateForFacebookParam(ggMsg)}', + ]; + final combinedMsg = parts.join(' | '); + final params = { + '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 = { + '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 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 = { + 'server_error_code': '$serverCode', + 'user_id': uid, + 'device_id': did, + if (missingUserId) 'register_faild': 'register faild', + }; + FacebookService.logEvent( + facebookLoginFailedEventName, + parameters: params, + ); + } } diff --git a/lib/src/services/image_presigned_upload_create_flow.dart b/lib/src/services/image_presigned_upload_create_flow.dart index 3c7a2ec..1837129 100644 --- a/lib/src/services/image_presigned_upload_create_flow.dart +++ b/lib/src/services/image_presigned_upload_create_flow.dart @@ -6,6 +6,7 @@ import '../entities/image_entities.dart'; import '../log/app_logger.dart'; import 'image_api.dart'; import 'image_compress.dart'; +import 'image_upload_expected_size_cache.dart'; import 'task_upload_cover_store.dart'; final _presignedPutLog = AppLogger('PresignedUpload'); @@ -99,6 +100,9 @@ abstract final class ImagePresignedUploadCreateTaskFlow { } final presigned = presignedRes.data!; + await ImageUploadExpectedSizeCache.writeImageExpectedSize( + presigned.expectedSize, + ); final uploadUrl = presigned.uploadUrl; final filePath = presigned.filePath; if (uploadUrl == null || diff --git a/lib/src/services/image_upload_expected_size_cache.dart b/lib/src/services/image_upload_expected_size_cache.dart new file mode 100644 index 0000000..e4ff0f2 --- /dev/null +++ b/lib/src/services/image_upload_expected_size_cache.dart @@ -0,0 +1,44 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// 缓存 [ImageApi.getUploadPresignedUrl] / [FeedbackApi.getUploadPresignedUrl] 响应中的 +/// [UploadPresignedUrlResponse.expectedSize] / [FeedbackUploadPresignedUrlResponse.expectedSize], +/// 供选图前展示与校验;未缓存时由调用方使用 [fallbackMaxBytes]。 +abstract final class ImageUploadExpectedSizeCache { + ImageUploadExpectedSizeCache._(); + + /// 未命中服务端下发的 `expectedSize` 时,客户端使用的默认上限(字节)。 + static const int fallbackMaxBytes = 20 * 1024 * 1024; + + static const String _kImage = + 'client_proxy_image_presigned_expected_size_bytes_v1'; + static const String _kFeedback = + 'client_proxy_feedback_presigned_expected_size_bytes_v1'; + + static Future readImageMaxBytesForUi() async { + final p = await SharedPreferences.getInstance(); + final v = p.getInt(_kImage); + if (v != null && v > 0) return v; + return fallbackMaxBytes; + } + + static Future readFeedbackMaxBytesForUi() async { + final p = await SharedPreferences.getInstance(); + final v = p.getInt(_kFeedback); + if (v != null && v > 0) return v; + return fallbackMaxBytes; + } + + /// 在生图预签名成功后写入;仅当 [bytes] 为正整数时持久化。 + static Future writeImageExpectedSize(int? bytes) async { + if (bytes == null || bytes <= 0) return; + final p = await SharedPreferences.getInstance(); + await p.setInt(_kImage, bytes); + } + + /// 在反馈预签名成功后写入;仅当 [bytes] 为正整数时持久化。 + static Future writeFeedbackExpectedSize(int? bytes) async { + if (bytes == null || bytes <= 0) return; + final p = await SharedPreferences.getInstance(); + await p.setInt(_kFeedback, bytes); + } +} diff --git a/lib/src/services/login_identity_cache.dart b/lib/src/services/login_identity_cache.dart new file mode 100644 index 0000000..2923145 --- /dev/null +++ b/lib/src/services/login_identity_cache.dart @@ -0,0 +1,43 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// 登录成功后缓存 [FrameworkAuthService] 使用的用户 ID 与设备 ID,供换皮应用读取(如离线展示、诊断)。 +/// +/// 与业务 token 无关;仅作本地副本。 +abstract final class LoginIdentityCache { + LoginIdentityCache._(); + + static const String _kUserId = 'client_proxy_framework_cached_login_user_id_v1'; + static const String _kDeviceId = + 'client_proxy_framework_cached_login_device_id_v1'; + + /// 最近一次成功登录写入的 `userId`;未写入过则为 `null`。 + static Future readCachedUserId() async { + final p = await SharedPreferences.getInstance(); + final v = p.getString(_kUserId)?.trim(); + if (v == null || v.isEmpty) return null; + return v; + } + + /// 最近一次成功登录写入的 `deviceId`;未写入过则为 `null`。 + static Future readCachedDeviceId() async { + final p = await SharedPreferences.getInstance(); + final v = p.getString(_kDeviceId)?.trim(); + if (v == null || v.isEmpty) return null; + return v; + } + + /// 在快速登录成功且 [userId] 非空时由框架调用。 + static Future writeUserAndDeviceId({ + required String userId, + required String deviceId, + }) async { + final uid = userId.trim(); + if (uid.isEmpty) return; + final did = deviceId.trim(); + final p = await SharedPreferences.getInstance(); + await p.setString(_kUserId, uid); + if (did.isNotEmpty) { + await p.setString(_kDeviceId, did); + } + } +} diff --git a/lib/src/services/user_api.dart b/lib/src/services/user_api.dart index 8a21654..14d52d8 100644 --- a/lib/src/services/user_api.dart +++ b/lib/src/services/user_api.dart @@ -7,7 +7,7 @@ import '../entities/user_entities.dart'; /// 用户相关 API(**业务逻辑字段名**,经 [FieldMapping] 映射为线网字段名) /// /// **请求头**:除 [UserApi.fastLogin] 外,需登录接口均由 [ProxyClient] 自动附带 -/// `pkg`(包名)与 `User_token`(已设置 token 时)。fast_login 仅带 `pkg`,不带 token。 +/// `pkg`(包名)与 `User_token`(已设置 token 时)。**fast_login** 保留 `pkg`,但不注入 `User_token`。 /// /// **请求体**:与《客户端指南》一致,使用**业务逻辑字段名**(如 `referer`、`deviceId`)。 abstract final class UserApi { @@ -15,8 +15,8 @@ abstract final class UserApi { /// 设备快速登录 /// - /// **请求头**:仅 `pkg`(无 `User_token`)。 - /// **Query**:`app`、`type`(默认 `fb`)、`pkg`、`ch`(可选)。 + /// **请求头**:保留 `pkg`,不注入 `User_token`(见 [ProxyClient.request] 的 `includeUserTokenInHeader`)。 + /// **Query**:`app`、`type`(未传时默认 `gg`,Google Play Install Referrer 归因)、`pkg`、`ch`(可选)。 /// **Body**:`referer`、`sign`、`deviceId`。 static Future> fastLogin({ required String deviceId, @@ -34,7 +34,7 @@ abstract final class UserApi { queryParams: { if (ch != null && ch.isNotEmpty) 'ch': ch, 'pkg': config.packageName, - 'type': type ?? 'fb', + 'type': type ?? 'gg', 'app': app, }, body: {