import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'api_config.dart'; import 'api_crypto.dart'; const _logTag = '[ProxyClient]'; void _log(String msg) { if (kDebugMode) debugPrint('$_logTag $msg'); } /// 代理请求体字段名(统一请求参数) abstract final class ProxyKeys { static const String heroClass = 'hero_class'; static const String petSpecies = 'pet_species'; static const String powerLevel = 'power_level'; static const String questRank = 'quest_rank'; static const String battleScore = 'battle_score'; static const String loyaltyIndex = 'loyalty_index'; static const String billingAddr = 'billing_addr'; static const String utmTerm = 'utm_term'; static const String clusterId = 'cluster_id'; static const String lsnValue = 'lsn_value'; static const String accuracyVal = 'accuracy_val'; static const String dirPath = 'dir_path'; } /// 代理请求客户端 class ProxyClient { ProxyClient({ this.baseUrl, this.packageName = ApiConfig.packageName, String? userToken, }) : _userToken = userToken; final String? baseUrl; final String packageName; String? _userToken; String? get userToken => _userToken; set userToken(String? value) => _userToken = value; String get _baseUrl => baseUrl ?? ApiConfig.baseUrl; /// 构建 V2 包装体,业务参数填入 sanctum Map _buildV2Wrapper(Map sanctum) { return { 'arsenal': 4, 'vault': { 'tome': { 'codex': { 'grimoire': { 'sanctum': sanctum, }, }, }, }, 'roar': ApiCrypto.randomAlnum(), 'clash': ApiCrypto.randomAlnum(), 'thunder': ApiCrypto.randomAlnum(), 'rumble': ApiCrypto.randomAlnum(), 'howl': ApiCrypto.randomAlnum(), 'growl': ApiCrypto.randomAlnum(), }; } /// 发送代理请求 /// [path] 接口路径,如 /v1/user/fast_login /// [method] HTTP 方法,POST 或 GET /// [headers] 请求头,使用 V2 字段名(portal、knight 等) /// [queryParams] 查询参数,使用 V2 字段名(sentinel、asset 等) /// [body] 请求体,使用 V2 字段名,将填入 sanctum Future request({ required String path, required String method, Map? headers, Map? queryParams, Map? body, }) async { final headersMap = Map.from(headers ?? {}); if (packageName.isNotEmpty) { headersMap['portal'] = packageName; } if (_userToken != null && _userToken!.isNotEmpty) { headersMap['knight'] = _userToken!; } final paramsMap = Map.from( queryParams?.map((k, v) => MapEntry(k, v)) ?? {}, ); final sanctum = body ?? {}; final v2Body = _buildV2Wrapper(sanctum); // 原始入参 final headersEncoded = jsonEncode(headersMap); final paramsEncoded = jsonEncode(paramsMap); final v2BodyEncoded = jsonEncode(v2Body); _log('========== 原始入参 =========='); _log('path: $path'); _log('method: $method'); _log('headers: $headersEncoded'); _log('queryParams: $paramsEncoded'); _log('body(sanctum): ${jsonEncode(sanctum)}'); _log('v2Body: $v2BodyEncoded'); final petSpeciesEnc = ApiCrypto.encrypt(path); final powerLevelEnc = ApiCrypto.encrypt(method); final questRankEnc = ApiCrypto.encrypt(headersEncoded); final battleScoreEnc = ApiCrypto.encrypt(paramsEncoded); final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded); _log('========== 加密后 =========='); _log('pet_species: $petSpeciesEnc'); _log('power_level: $powerLevelEnc'); _log('quest_rank: $questRankEnc'); _log('battle_score: $battleScoreEnc'); _log('loyalty_index: $loyaltyIndexEnc'); final proxyBody = { ProxyKeys.heroClass: ApiConfig.appId, ProxyKeys.petSpecies: petSpeciesEnc, ProxyKeys.powerLevel: powerLevelEnc, ProxyKeys.questRank: questRankEnc, ProxyKeys.battleScore: battleScoreEnc, ProxyKeys.loyaltyIndex: loyaltyIndexEnc, ProxyKeys.billingAddr: ApiCrypto.randomBase64(), ProxyKeys.utmTerm: ApiCrypto.randomBase64(), ProxyKeys.clusterId: ApiCrypto.randomBase64(), ProxyKeys.lsnValue: ApiCrypto.randomBase64(), ProxyKeys.accuracyVal: ApiCrypto.randomBase64(), ProxyKeys.dirPath: ApiCrypto.randomBase64(), }; final url = '$_baseUrl${ApiConfig.proxyPath}'; _log('========== 请求 URL =========='); _log('$url'); final response = await http.post( Uri.parse(url), headers: {'Content-Type': 'application/json'}, body: jsonEncode(proxyBody), ); _log('========== 响应 =========='); _log('statusCode: ${response.statusCode}'); _log('body: ${response.body}'); return _parseResponse(response); } ApiResponse _parseResponse(http.Response response) { try { // 响应解密流程:Base64 解码 → AES-ECB 解密 → PKCS5 去填充 → UTF-8 字符串 final decrypted = ApiCrypto.decrypt(response.body); final json = jsonDecode(decrypted) as Map; _log('json: $json'); // 解析 helm=code, rampart=msg, sidekick=data final sanctum = json['vault']?['tome']?['codex']?['grimoire']?['sanctum']; if (sanctum is Map) { return ApiResponse( code: sanctum['helm'] as int? ?? -1, msg: sanctum['rampart'] as String? ?? '', data: sanctum['sidekick'], ); } return ApiResponse( code: json['helm'] as int? ?? -1, msg: json['rampart'] as String? ?? '', data: json['sidekick'], ); } catch (e) { return ApiResponse(code: -1, msg: e.toString()); } } } /// 统一 API 响应 class ApiResponse { ApiResponse({ required this.code, this.msg = '', this.data, }); final int code; final String msg; final dynamic data; bool get isSuccess => code == 0; }