import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../config/app_config.dart'; import '../entities/entity.dart'; import '../log/app_logger.dart'; import 'api_crypto.dart'; import 'api_response.dart'; final _proxyLog = AppLogger('ProxyClient'); const int _maxLogChunk = 1000; void _logLong(String text) { if (text.isEmpty) return; final lines = text.split('\n'); if (lines.length == 1 && text.length <= _maxLogChunk) { _proxyLog.d(text); return; } final buffer = StringBuffer(); int chunkIndex = 0; for (final line in lines) { final lineWithNewline = buffer.isEmpty ? line : '\n$line'; if (buffer.length + lineWithNewline.length > _maxLogChunk && buffer.isNotEmpty) { chunkIndex++; _proxyLog.d('(part $chunkIndex)\n$buffer'); buffer.clear(); buffer.write(line); } else { if (buffer.isNotEmpty) buffer.write('\n'); buffer.write(line); } } if (buffer.isNotEmpty) { chunkIndex++; _proxyLog .d(chunkIndex > 1 ? '(part $chunkIndex)\n$buffer' : buffer.toString()); } } void _log(Object? msg) { if (!kDebugMode) return; final str = msg?.toString().trim() ?? ''; if (str.length <= _maxLogChunk) { _proxyLog.d(str); return; } _logLong(str); } dynamic _getByPath(Map json, List path) { dynamic current = json; for (final key in path) { if (current is! Map) return null; current = current[key]; } return current; } /// 实体工厂函数类型 typedef EntityFactory = T Function(Map); /// 代理请求客户端 class ProxyClient { ProxyClient({ required this.config, this.baseUrlOverride, this.userToken, }) : _crypto = ApiCrypto(aesKey: config.aesKey); final AppConfig config; final String? baseUrlOverride; final ApiCrypto _crypto; String? userToken; String get _baseUrl => baseUrlOverride ?? config.baseUrl; Map _buildV2Wrapper(Map sanctum) { final result = Map.from(config.v2FixedValues); Map current = result; final path = config.v2SanctumPath; for (var i = 0; i < path.length - 1; i++) { final key = path[i]; current[key] = {}; current = current[key] as Map; } current[path.last] = sanctum; for (final key in config.v2NoiseKeys) { result[key] = ApiCrypto.randomAlnum(); } return result; } /// 发送代理请求(返回原始字段的 Map) /// /// [headers]、[queryParams]、[body] 使用**原始字段名**, /// 框架会按 [AppConfig.fieldMapping] 转为 V2 字段名后发送。 /// 响应 data 会自动从 V2 转回原始字段名。 Future request({ required String path, required String method, Map? headers, Map? queryParams, Map? body, }) async { final pk = config.proxyKeys; final mapping = config.fieldMapping; var headersMap = Map.from(headers ?? {}); if (config.packageName.isNotEmpty) { headersMap[mapping.headerPackageNameField] = config.packageName; } if (userToken != null && userToken!.isNotEmpty) { headersMap[mapping.headerUserTokenField] = userToken!; } headersMap = mapping.mapRequest(headersMap); var paramsMap = Map.from( queryParams?.map((k, v) => MapEntry(k, v)) ?? {}, ); paramsMap = mapping.mapRequest(paramsMap); var sanctum = body ?? {}; sanctum = mapping.mapRequest(sanctum); final v2Body = _buildV2Wrapper(sanctum); final headersEncoded = jsonEncode(headersMap); final paramsEncoded = jsonEncode(paramsMap); final v2BodyEncoded = jsonEncode(v2Body); final logStr = '========== 原始入参 ===========\npath: $path\nmethod: $method\nqueryParams: $paramsEncoded\nbody(sanctum): ${jsonEncode(sanctum)}'; logWithEmbeddedJson(logStr); final proxyBody = { pk.appIdField: config.appName, pk.pathField: _crypto.encrypt(path), pk.methodField: _crypto.encrypt(method), pk.headerField: _crypto.encrypt(headersEncoded), pk.paramsField: _crypto.encrypt(paramsEncoded), pk.bodyField: _crypto.encrypt(v2BodyEncoded), }; for (final key in pk.noiseKeys) { proxyBody[key] = ApiCrypto.randomBase64(); } final url = '$_baseUrl${config.proxyPath}'; logWithEmbeddedJson('========== 实际请求体 ===========\n${jsonEncode(proxyBody)}'); final response = await http.post( Uri.parse(url), headers: {'Content-Type': 'application/json'}, body: jsonEncode(proxyBody), ); return _parseResponse(response); } /// 发送代理请求并返回实体 /// /// [headers]、[queryParams]、[body] 使用**原始字段名**。 /// [entityFactory] 用于将映射后的 data 转换为实体对象。 Future> requestEntity({ required String path, required String method, required EntityFactory entityFactory, Map? headers, Map? queryParams, Map? body, }) async { final response = await request( path: path, method: method, headers: headers, queryParams: queryParams, body: body, ); if (response.isSuccess && response.data is Map) { final entity = entityFactory(response.data as Map); return EntityResponse( code: response.code, msg: response.msg, data: entity, ); } return EntityResponse( code: response.code, msg: response.msg, data: null, ); } ApiResponse _parseResponse(http.Response response) { try { final decrypted = _crypto.decrypt(response.body); final json = jsonDecode(decrypted) as Map; logWithEmbeddedJson('========== 响应 ===========\n${jsonEncode(json)}'); final mapping = config.fieldMapping; final sanctum = _getByPath(json, config.v2SanctumPath); final codeField = mapping.responseCodeField; final msgField = mapping.responseMsgField; final dataField = mapping.responseDataField; final code = sanctum is Map ? (sanctum[codeField] as int? ?? -1) : (json[codeField] as int? ?? -1); final msg = sanctum is Map ? (sanctum[msgField] as String? ?? '') : (json[msgField] as String? ?? ''); var data = sanctum is Map ? sanctum[dataField] : json[dataField]; if (data is Map) { data = mapping.mapResponse(data); } return ApiResponse(code: code, msg: msg, data: data); } catch (e) { return ApiResponse(code: -1, msg: e.toString()); } } } /// 带泛型实体的响应 class EntityResponse { EntityResponse({ required this.code, this.msg = '', this.data, }); final int code; final String msg; final T? data; bool get isSuccess => code == 0; }