feat(core): 请求头统一上报设备系统与机型
- 新增 DeviceProfileReader,首次读取后缓存,插件不可用时安全降级 - 拦截器为所有请求补 X-Client-Platform / Os / Model,匿名请求同样携带
This commit is contained in:
1 parent
929b7f72f8
commit
d6df5fbe5f
6 files changed
+148
-21
No files matched your search
@@ -14,6 +14,7 @@ import 'core/data/remote/catalog_api.dart';
|
||||
import 'core/data/remote/category_api.dart';
|
||||
import 'core/data/remote/comment_api.dart';
|
||||
import 'core/data/remote/commerce_api.dart';
|
||||
import 'core/data/remote/device_profile.dart';
|
||||
import 'core/data/remote/download_api.dart';
|
||||
import 'core/data/remote/interaction_api.dart';
|
||||
import 'core/data/remote/library_api.dart';
|
||||
@@ -116,6 +117,9 @@ class LumaDramaApp extends StatefulWidget {
|
||||
class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
late final AuthSessionStore _sessionStore = AuthSessionStore();
|
||||
|
||||
/// 设备画像只读一次:供拦截器在每次请求头里带上系统与机型。
|
||||
late final DeviceProfileReader _deviceProfile = DeviceProfileReader();
|
||||
|
||||
/// 全 App 共用一个 ApiClient:这样目录、账号、后续的互动/进度请求
|
||||
/// 都自动带 `Authorization` 与 `X-Device-Id`,401 的刷新重试也只有一处实现。
|
||||
late final ApiClient _apiClient = ApiClient(
|
||||
@@ -123,6 +127,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
tokenRefresher: () => _auth.refresh(),
|
||||
onUnauthorized: () => _auth.clear(),
|
||||
deviceIdReader: _sessionStore.deviceId,
|
||||
deviceProfileReader: _deviceProfile.read,
|
||||
);
|
||||
|
||||
late final AuthController _auth = AuthController(
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:dio/dio.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import 'api_exception.dart';
|
||||
import 'api_interceptors.dart';
|
||||
import 'device_profile.dart';
|
||||
import 'network_log_interceptor.dart';
|
||||
|
||||
/// `ApiException` 从这里也能拿到:历史调用方一律 `import 'api_client.dart'`,不必改 import。
|
||||
@@ -24,6 +25,7 @@ class ApiClient {
|
||||
this.tokenRefresher,
|
||||
this.onUnauthorized,
|
||||
this.deviceIdReader,
|
||||
this.deviceProfileReader,
|
||||
}) : _fixedBaseUrl = baseUrl {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
@@ -40,6 +42,7 @@ class ApiClient {
|
||||
AuthHeaderInterceptor(
|
||||
accessTokenReader: accessTokenReader,
|
||||
deviceIdReader: deviceIdReader,
|
||||
deviceProfileReader: deviceProfileReader,
|
||||
),
|
||||
const EnvelopeInterceptor(),
|
||||
NetworkLogInterceptor(),
|
||||
@@ -67,6 +70,9 @@ class ApiClient {
|
||||
/// 设备号(`X-Device-Id`):注册风控、后续下载绑定都要用
|
||||
final Future<String> Function()? deviceIdReader;
|
||||
|
||||
/// 设备画像(系统/机型版本):后台列表展示客户端系统用
|
||||
final Future<DeviceProfile> Function()? deviceProfileReader;
|
||||
|
||||
Future<Object?> get(String path, {Map<String, String>? query}) {
|
||||
return _request('GET', path, query: query);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'api_exception.dart';
|
||||
import 'device_profile.dart';
|
||||
import 'network_log_interceptor.dart';
|
||||
|
||||
/// 请求级开关:放在 `RequestOptions.extra` 里,由 `ApiClient` 写入、拦截器读取。
|
||||
@@ -32,7 +33,11 @@ class TokenExpiredException implements Exception {
|
||||
///
|
||||
/// 每个请求都重新取一次令牌,所以刷新成功后重发的那次会自动带上新令牌。
|
||||
class AuthHeaderInterceptor extends Interceptor {
|
||||
AuthHeaderInterceptor({this.accessTokenReader, this.deviceIdReader});
|
||||
AuthHeaderInterceptor({
|
||||
this.accessTokenReader,
|
||||
this.deviceIdReader,
|
||||
this.deviceProfileReader,
|
||||
});
|
||||
|
||||
/// 当前 access token;未登录返回 null 或空串。
|
||||
final String? Function()? accessTokenReader;
|
||||
@@ -40,6 +45,9 @@ class AuthHeaderInterceptor extends Interceptor {
|
||||
/// 设备号(`X-Device-Id`):注册风控、下载绑定都用它。
|
||||
final Future<String> Function()? deviceIdReader;
|
||||
|
||||
/// 设备画像(系统/机型):后台判断客户端系统用,匿名请求同样要带。
|
||||
final Future<DeviceProfile> Function()? deviceProfileReader;
|
||||
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
@@ -54,6 +62,10 @@ class AuthHeaderInterceptor extends Interceptor {
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
options.headers['X-Device-Id'] = deviceId;
|
||||
}
|
||||
final profile = await deviceProfileReader?.call();
|
||||
if (profile != null) {
|
||||
options.headers.addAll(profile.headers);
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 设备画像:随每个请求以请求头上报,供后台列表识别客户端系统。
|
||||
///
|
||||
/// 之前只有登录接口在 body 里带 `platform` / `osVersion`,匿名互动没有任何系统信息,
|
||||
/// 后台只能显示「—」。改成请求头后,点赞、收藏、评论、观看等接口都能带上。
|
||||
class DeviceProfile {
|
||||
const DeviceProfile({
|
||||
this.platform = '',
|
||||
this.osVersion = '',
|
||||
this.model = '',
|
||||
});
|
||||
|
||||
/// ANDROID / IOS / WEB;桌面端等无法归类时为空。
|
||||
final String platform;
|
||||
|
||||
/// 系统版本,Android 取 release(如 14),iOS 取 systemVersion(如 17.2)。
|
||||
final String osVersion;
|
||||
|
||||
/// 机型,Android 取 model(如 SM-G991B),iOS 取 utsname.machine(如 iPhone15,2)。
|
||||
final String model;
|
||||
|
||||
Map<String, String> get headers => {
|
||||
if (platform.isNotEmpty) 'X-Client-Platform': platform,
|
||||
if (osVersion.isNotEmpty) 'X-Client-Os': osVersion,
|
||||
if (model.isNotEmpty) 'X-Client-Model': model,
|
||||
};
|
||||
}
|
||||
|
||||
/// 读取一次并缓存设备画像:插件调用有开销,且整个进程内不会变。
|
||||
class DeviceProfileReader {
|
||||
DeviceProfileReader({DeviceInfoPlugin? plugin})
|
||||
: _plugin = plugin ?? DeviceInfoPlugin();
|
||||
|
||||
final DeviceInfoPlugin _plugin;
|
||||
Future<DeviceProfile>? _cached;
|
||||
|
||||
Future<DeviceProfile> read() => _cached ??= _load();
|
||||
|
||||
Future<DeviceProfile> _load() async {
|
||||
if (kIsWeb) return const DeviceProfile(platform: 'WEB');
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final info = await _plugin.androidInfo;
|
||||
return DeviceProfile(
|
||||
platform: 'ANDROID',
|
||||
osVersion: info.version.release,
|
||||
model: info.model,
|
||||
);
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
final info = await _plugin.iosInfo;
|
||||
return DeviceProfile(
|
||||
platform: 'IOS',
|
||||
osVersion: info.systemVersion,
|
||||
model: info.utsname.machine,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// 插件不可用(widget 测试没有原生通道)时退化为 dart:io 能拿到的信息
|
||||
}
|
||||
return const DeviceProfile();
|
||||
}
|
||||
}
|
||||
+52
-20
@@ -113,6 +113,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.3"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -216,7 +232,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
html:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
|
||||
@@ -248,10 +264,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
version: "0.20.3"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -328,10 +344,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
version: "0.12.20"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -344,10 +360,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -380,14 +396,6 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -444,6 +452,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -613,10 +629,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.12"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,10 +653,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.4.3"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -705,6 +721,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.5"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -730,5 +762,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
sdks:
|
||||
dart: ">=3.12.2 <4.0.0"
|
||||
dart: ">=3.13.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
@@ -48,6 +48,9 @@ dependencies:
|
||||
sdk: flutter
|
||||
path_provider: 2.1.5
|
||||
html: ^0.15.6
|
||||
# 设备画像上报(系统版本 + 机型),后台列表据此展示客户端系统;
|
||||
# 用缓存里的 9.0.3,避免拉新版带来的原生构建变更。
|
||||
device_info_plus: 9.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in new issue
Block a user