import 'dart:io'; import 'dart:math'; import 'package:dio/dio.dart'; import '../storage/storage_manager.dart'; import 'dio_client.dart'; import 'http_error.dart'; import 'interceptors/response_interceptor.dart'; /// 统一 HTTP 封装 — 光鸭云盘 class Http { /// 通用底层请求方法 static Future request( String path, { String method = 'POST', Map? queryParameters, dynamic data, Map? headers, CancelToken? cancelToken, Options? options, bool useAccountDio = false, bool allowAnyCode = false, }) async { final client = useAccountDio ? DioClient.accountDio : DioClient.dio; final mergedOptions = (options ?? Options()).copyWith( method: method, headers: headers, ); try { final res = await client.request( path, data: data, queryParameters: queryParameters, cancelToken: cancelToken, options: mergedOptions, ); return res.data as T; } on DioException catch (e) { // 将业务错误信息包装为 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); } rethrow; } catch (e) { rethrow; } } /// 光鸭 API 请求(带 token,code==200 为成功) static Future> apiRequest( String path, { String method = 'POST', dynamic body, Map? headers, CancelToken? cancelToken, List allowedCodes = const [], }) async { // 过期自动刷新 _checkTokenExpiry(); final mergedHeaders = { 'traceparent': _generateTraceparent(), ...?headers, }; final data = await request>( path, method: method, data: body, headers: mergedHeaders, cancelToken: cancelToken, options: Options(extra: {ResponseInterceptor.skipCodeCheckKey: true}), ); final code = _businessCode(data['code']); if (code == null || code == 0 || code == 200 || allowedCodes.contains(code)) { return data; } throw ApiException( status: code, message: extractHttpMessage(data) ?? '请求失败 ($code)', ); } /// 光鸭账号请求(account API 响应格式无 code 字段,跳过业务 code 检查) static Future> accountRequest( String path, { String method = 'POST', Map? body, Map? extraHeaders, bool authenticated = false, }) async { final headers = { ..._accountHeaders(), 'did': _getDeviceID(), 'dt': '4', ...?extraHeaders, }; if (authenticated) { final token = StorageManager.get(StorageKeys.accessToken); if (token != null && token.isNotEmpty) { headers['authorization'] = 'Bearer $token'; } } final data = await request>( path, method: method, data: body, headers: headers, useAccountDio: true, options: Options(extra: {ResponseInterceptor.skipCodeCheckKey: true}), ); return data; } /// 公开请求(无 token,用于分享预览等) static Future> publicRequest( String path, { Map? body, }) async { return request>( path, method: 'POST', data: body, headers: {'traceparent': _generateTraceparent()}, ); } // ── Token 过期检测 ───────────────────────────────────────────── static void _checkTokenExpiry() { final expiresAt = StorageManager.get(StorageKeys.tokenExpiresAt); if (expiresAt == null) return; if (DateTime.now().millisecondsSinceEpoch > expiresAt) {} } static String _getDeviceID() { return StorageManager.get(StorageKeys.deviceID) ?? 'flutter-${DateTime.now().millisecondsSinceEpoch}'; } /// 账号网关会校验网页客户端的设备标识;与源客户端保持一致。 static Map _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() { final now = DateTime.now().microsecondsSinceEpoch; return '00-${now.toRadixString(16).padLeft(32, '0')}-${now.toRadixString(16).padLeft(16, '0')}-01'; } static int? _businessCode(dynamic value) { if (value == null) return null; if (value is int) return value; return int.tryParse(value.toString()); } }