fix account profile and improve operation logs
This commit is contained in:
+41
-26
@@ -1,4 +1,5 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
@@ -28,10 +29,6 @@ class Http {
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final endpoint = useAccountDio ? 'account' : 'api';
|
||||
log('[HTTP:$endpoint] -> $method $path');
|
||||
|
||||
try {
|
||||
final res = await client.request(
|
||||
path,
|
||||
@@ -40,23 +37,10 @@ class Http {
|
||||
cancelToken: cancelToken,
|
||||
options: mergedOptions,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
log(
|
||||
'[HTTP:$endpoint] <- $method $path status=${res.statusCode} '
|
||||
'elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
|
||||
return res.data as T;
|
||||
} on DioException catch (e) {
|
||||
stopwatch.stop();
|
||||
final status = e.response?.statusCode;
|
||||
log(
|
||||
'[HTTP:$endpoint] !! $method $path status=${status ?? '-'} '
|
||||
'type=${e.type.name} elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
|
||||
// 将业务错误信息包装为 ApiException
|
||||
final status = e.response?.statusCode;
|
||||
final errorMsg = e.error is String ? (e.error as String) : '';
|
||||
if (errorMsg.isNotEmpty) {
|
||||
throw ApiException(status: status, message: errorMsg);
|
||||
@@ -64,10 +48,6 @@ class Http {
|
||||
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
stopwatch.stop();
|
||||
log(
|
||||
'[HTTP:$endpoint] !! $method $path elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +101,7 @@ class Http {
|
||||
bool authenticated = false,
|
||||
}) async {
|
||||
final headers = <String, dynamic>{
|
||||
..._accountHeaders(),
|
||||
'did': _getDeviceID(),
|
||||
'dt': '4',
|
||||
...?extraHeaders,
|
||||
@@ -163,9 +144,7 @@ class Http {
|
||||
static void _checkTokenExpiry() {
|
||||
final expiresAt = StorageManager.get<int>(StorageKeys.tokenExpiresAt);
|
||||
if (expiresAt == null) return;
|
||||
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {
|
||||
log('[HTTP] token expired, will refresh on next 401');
|
||||
}
|
||||
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {}
|
||||
}
|
||||
|
||||
static String _getDeviceID() {
|
||||
@@ -173,6 +152,42 @@ class Http {
|
||||
'flutter-${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
/// 账号网关会校验网页客户端的设备标识;与源客户端保持一致。
|
||||
static Map<String, String> _accountHeaders() {
|
||||
final deviceID = _getDeviceID();
|
||||
return {
|
||||
'x-client-id': 'aMe-8VSlkrbQXpUR',
|
||||
'x-client-version': '0.0.1',
|
||||
'x-device-id': deviceID,
|
||||
'x-device-model': 'chrome%2F150.0.0.0',
|
||||
'x-device-name': 'PC-Chrome',
|
||||
'x-device-sign': 'wdi10.$deviceID${_randomHex(16)}',
|
||||
'x-net-work-type': 'NONE',
|
||||
'x-os-version': _webPlatformName(),
|
||||
'x-platform-version': '1',
|
||||
'x-protocol-version': '301',
|
||||
'x-provider-name': 'NONE',
|
||||
'x-sdk-version': '9.0.2',
|
||||
};
|
||||
}
|
||||
|
||||
static String _webPlatformName() {
|
||||
if (Platform.isMacOS) return 'MacIntel';
|
||||
if (Platform.isWindows) return 'Win32';
|
||||
if (Platform.isLinux) return 'Linux x86_64';
|
||||
if (Platform.isIOS) return 'iPhone';
|
||||
if (Platform.isAndroid) return 'Linux armv8l';
|
||||
return Platform.operatingSystem;
|
||||
}
|
||||
|
||||
static String _randomHex(int bytes) {
|
||||
final random = Random.secure();
|
||||
return List.generate(
|
||||
bytes,
|
||||
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||
).join();
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
static String _generateTraceparent() {
|
||||
|
||||
@@ -1,34 +1,52 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../http_error.dart';
|
||||
import '../../logging/app_logger.dart';
|
||||
|
||||
class AppLogInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
AppLogger.debug(
|
||||
'HTTP',
|
||||
'→ ${options.method} ${options.uri.path}${options.uri.hasQuery ? '?${options.uri.query}' : ''}',
|
||||
);
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
AppLogger.debug(
|
||||
'HTTP',
|
||||
'← ${response.statusCode ?? 0} ${response.requestOptions.method} ${response.requestOptions.uri.path}',
|
||||
);
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
final status = err.response?.statusCode;
|
||||
final serverMessage = err.response?.data is Map
|
||||
? extractHttpMessage(err.response?.data)
|
||||
: null;
|
||||
final detail = serverMessage?.trim().isNotEmpty == true
|
||||
? serverMessage!
|
||||
: (err.error?.toString().trim().isNotEmpty == true
|
||||
? err.error.toString()
|
||||
: err.type.name);
|
||||
AppLogger.error(
|
||||
'HTTP',
|
||||
'${err.requestOptions.method} ${err.requestOptions.uri.path} 失败',
|
||||
error: err.message,
|
||||
stackTrace: err.stackTrace,
|
||||
'${_actionName(err.requestOptions.path)}失败(状态码 ${status ?? '-'},原因:$detail)',
|
||||
);
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
static String _actionName(String path) {
|
||||
if (path.contains('/v1/user/me')) return '获取账户资料';
|
||||
if (path.contains('/assets/v1/get_assets')) return '获取云盘空间信息';
|
||||
if (path.contains('/search_files')) return '搜索云盘文件';
|
||||
if (path.contains('/get_file_list')) return '读取文件列表';
|
||||
if (path.contains('/get_file_detail')) return '读取文件详情';
|
||||
if (path.contains('/create_dir')) return '新建文件夹';
|
||||
if (path.contains('/move_file')) return '移动文件';
|
||||
if (path.contains('/copy_file')) return '复制文件';
|
||||
if (path.contains('/rename')) return '重命名文件';
|
||||
if (path.contains('/recycle_file')) return '移入回收站';
|
||||
if (path.contains('/delete_file')) return '删除文件';
|
||||
if (path.contains('/auth/token')) return '刷新登录凭据';
|
||||
if (path.contains('/auth/')) return '登录认证';
|
||||
if (path.contains('/tmdb')) return '获取影视资料';
|
||||
return '云盘服务请求';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../logging/app_logger.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../storage/storage_manager.dart';
|
||||
import '../http_error.dart';
|
||||
@@ -60,10 +59,6 @@ class AuthInterceptor extends Interceptor {
|
||||
final status = err.response?.statusCode;
|
||||
final alreadyLoggedOut = status == 401 && _isAlreadyLoggedOut;
|
||||
|
||||
if (!alreadyLoggedOut) {
|
||||
log('[Auth] error path=${err.requestOptions.path} status=$status');
|
||||
}
|
||||
|
||||
// 免认证路径的错误直接放行
|
||||
if (_isAuthExemptPath(err.requestOptions.path)) {
|
||||
return handler.next(err);
|
||||
@@ -127,7 +122,7 @@ class AuthInterceptor extends Interceptor {
|
||||
|
||||
// 开始刷新
|
||||
_isRefreshing = true;
|
||||
log('[Auth] refreshing access token');
|
||||
AppLogger.info('Auth', '登录凭据已过期,正在刷新');
|
||||
|
||||
try {
|
||||
final dio = Dio(
|
||||
@@ -158,7 +153,7 @@ class AuthInterceptor extends Interceptor {
|
||||
),
|
||||
);
|
||||
|
||||
log('[Auth] access token refreshed');
|
||||
AppLogger.info('Auth', '登录凭据刷新完成');
|
||||
|
||||
final data = res.data;
|
||||
final newAccess = _findStringDeep(data, ['access_token', 'accessToken']);
|
||||
@@ -202,7 +197,7 @@ class AuthInterceptor extends Interceptor {
|
||||
final response = await DioClient.dio.fetch(request);
|
||||
return handler.resolve(response);
|
||||
} catch (e) {
|
||||
log('[Auth] token refresh failed: $e');
|
||||
AppLogger.warning('Auth', '登录凭据刷新失败,将退出当前账号');
|
||||
|
||||
for (var c in _waitQueue) {
|
||||
c.complete();
|
||||
@@ -228,7 +223,7 @@ class AuthInterceptor extends Interceptor {
|
||||
if (_logoutScheduled) return;
|
||||
_logoutScheduled = true;
|
||||
|
||||
log('[Auth] logout scheduled');
|
||||
AppLogger.info('Auth', '登录状态已失效,正在退出当前账号');
|
||||
await StorageManager.delete(StorageKeys.accessToken);
|
||||
await StorageManager.delete(StorageKeys.refreshToken);
|
||||
await StorageManager.delete(StorageKeys.tokenExpiresAt);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../http_error.dart';
|
||||
@@ -72,27 +70,6 @@ class ResponseInterceptor extends Interceptor {
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// 网络错误
|
||||
if (err.type == DioExceptionType.connectionError ||
|
||||
err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.sendTimeout ||
|
||||
err.type == DioExceptionType.receiveTimeout) {
|
||||
if (!suppressErrorToast(err.requestOptions)) {
|
||||
log(
|
||||
'[HTTP] ${requestToastMessage(err.requestOptions, '请求超时或连接失败,请检查网络')}',
|
||||
);
|
||||
}
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// 其他错误
|
||||
if (!suppressErrorToast(err.requestOptions)) {
|
||||
final msg = err.response?.data is Map
|
||||
? extractHttpMessage(err.response?.data) ?? '请求失败'
|
||||
: '请求失败';
|
||||
log('[HTTP] ${requestToastMessage(err.requestOptions, msg)}');
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,26 @@ class AppLogEntry {
|
||||
});
|
||||
|
||||
String get text =>
|
||||
'${timestamp.toIso8601String()} [${level.name.toUpperCase()}] '
|
||||
'[$scope] $message';
|
||||
'${timestamp.toIso8601String()} [${_levelLabel(level)}] '
|
||||
'[${_scopeLabel(scope)}] $message';
|
||||
|
||||
static String _levelLabel(AppLogLevel level) => switch (level) {
|
||||
AppLogLevel.debug => '调试',
|
||||
AppLogLevel.info => '信息',
|
||||
AppLogLevel.warning => '警告',
|
||||
AppLogLevel.error => '错误',
|
||||
};
|
||||
|
||||
static String _scopeLabel(String scope) => switch (scope) {
|
||||
'HTTP' => '网络',
|
||||
'Auth' => '认证',
|
||||
'Media' => '媒体库',
|
||||
'CloudIndex' => '全盘索引',
|
||||
'App' => '应用',
|
||||
'Flutter' => '界面',
|
||||
'Dart' => '运行时',
|
||||
_ => scope,
|
||||
};
|
||||
}
|
||||
|
||||
/// One log sink for debug console, release file diagnostics and the in-app log
|
||||
|
||||
Reference in New Issue
Block a user