249 lines
7.1 KiB
Dart
249 lines
7.1 KiB
Dart
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;
|
||
}
|