client_framework/lib/src/api/proxy_client.dart
2026-03-26 10:39:39 +08:00

249 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, dynamic> json, List<String> path) {
dynamic current = json;
for (final key in path) {
if (current is! Map<String, dynamic>) return null;
current = current[key];
}
return current;
}
/// 实体工厂函数类型
typedef EntityFactory<T extends Entity> = T Function(Map<String, dynamic>);
/// 代理请求客户端
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<String, dynamic> _buildV2Wrapper(Map<String, dynamic> sanctum) {
final result = Map<String, dynamic>.from(config.v2FixedValues);
Map<String, dynamic> current = result;
final path = config.v2SanctumPath;
for (var i = 0; i < path.length - 1; i++) {
final key = path[i];
current[key] = <String, dynamic>{};
current = current[key] as Map<String, dynamic>;
}
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<ApiResponse> request({
required String path,
required String method,
Map<String, String>? headers,
Map<String, String>? queryParams,
Map<String, dynamic>? body,
}) async {
final pk = config.proxyKeys;
final mapping = config.fieldMapping;
var headersMap = Map<String, dynamic>.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<String, dynamic>.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 = <String, dynamic>{
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<EntityResponse<T>> requestEntity<T extends Entity>({
required String path,
required String method,
required EntityFactory<T> entityFactory,
Map<String, String>? headers,
Map<String, String>? queryParams,
Map<String, dynamic>? body,
}) async {
final response = await request(
path: path,
method: method,
headers: headers,
queryParams: queryParams,
body: body,
);
if (response.isSuccess && response.data is Map<String, dynamic>) {
final entity = entityFactory(response.data as Map<String, dynamic>);
return EntityResponse<T>(
code: response.code,
msg: response.msg,
data: entity,
);
}
return EntityResponse<T>(
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<String, dynamic>;
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<String, dynamic>
? (sanctum[codeField] as int? ?? -1)
: (json[codeField] as int? ?? -1);
final msg = sanctum is Map<String, dynamic>
? (sanctum[msgField] as String? ?? '')
: (json[msgField] as String? ?? '');
var data = sanctum is Map<String, dynamic>
? sanctum[dataField]
: json[dataField];
if (data is Map<String, dynamic>) {
data = mapping.mapResponse(data);
}
return ApiResponse(code: code, msg: msg, data: data);
} catch (e) {
return ApiResponse(code: -1, msg: e.toString());
}
}
}
/// 带泛型实体的响应
class EntityResponse<T extends Entity> {
EntityResponse({
required this.code,
this.msg = '',
this.data,
});
final int code;
final String msg;
final T? data;
bool get isSuccess => code == 0;
}