From d6df5fbe5fc1aba5beaa04d6e0b52019898376ee Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Tue, 27 Oct 2026 13:54:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E8=AF=B7=E6=B1=82=E5=A4=B4?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=8A=E6=8A=A5=E8=AE=BE=E5=A4=87=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=B8=8E=E6=9C=BA=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 DeviceProfileReader,首次读取后缓存,插件不可用时安全降级 - 拦截器为所有请求补 X-Client-Platform / Os / Model,匿名请求同样携带 --- lib/app.dart | 5 ++ lib/core/data/remote/api_client.dart | 6 ++ lib/core/data/remote/api_interceptors.dart | 14 ++++- lib/core/data/remote/device_profile.dart | 69 +++++++++++++++++++++ pubspec.lock | 72 ++++++++++++++++------ pubspec.yaml | 3 + 6 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 lib/core/data/remote/device_profile.dart diff --git a/lib/app.dart b/lib/app.dart index c47a354..e7e2503 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -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 { 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 { tokenRefresher: () => _auth.refresh(), onUnauthorized: () => _auth.clear(), deviceIdReader: _sessionStore.deviceId, + deviceProfileReader: _deviceProfile.read, ); late final AuthController _auth = AuthController( diff --git a/lib/core/data/remote/api_client.dart b/lib/core/data/remote/api_client.dart index e37543e..0c04f1b 100644 --- a/lib/core/data/remote/api_client.dart +++ b/lib/core/data/remote/api_client.dart @@ -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 Function()? deviceIdReader; + /// 设备画像(系统/机型版本):后台列表展示客户端系统用 + final Future Function()? deviceProfileReader; + Future get(String path, {Map? query}) { return _request('GET', path, query: query); } diff --git a/lib/core/data/remote/api_interceptors.dart b/lib/core/data/remote/api_interceptors.dart index cee6819..5962190 100644 --- a/lib/core/data/remote/api_interceptors.dart +++ b/lib/core/data/remote/api_interceptors.dart @@ -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 Function()? deviceIdReader; + /// 设备画像(系统/机型):后台判断客户端系统用,匿名请求同样要带。 + final Future Function()? deviceProfileReader; + @override Future 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); } } diff --git a/lib/core/data/remote/device_profile.dart b/lib/core/data/remote/device_profile.dart new file mode 100644 index 0000000..3115bd1 --- /dev/null +++ b/lib/core/data/remote/device_profile.dart @@ -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 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? _cached; + + Future read() => _cached ??= _load(); + + Future _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(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 156b4f0..a4e2eaf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 0c1a46b..8537396 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: