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
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user