hi-client/lib/app/network/http_util.dart

439 lines
14 KiB
Dart
Executable File
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 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
import 'package:flutter_loggy_dio/flutter_loggy_dio.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/network/base_response.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/utils/kr_http_adapter_util.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
// import 'package:crypto/crypto.dart';
// import 'package:encrypt/encrypt.dart';
import 'package:loggy/loggy.dart';
import '../utils/kr_aes_util.dart';
import '../utils/kr_log_util.dart';
// import 'package:video/app/utils/common_util.dart';
// import 'package:video/app/utils/log_util.dart';
/// 定义请求方法的枚举
enum HttpMethod { GET, POST, DELETE, PUT }
/// 封装请求
class HttpUtil {
final Dio _dio = Dio();
static final HttpUtil _instance = HttpUtil._internal();
HttpUtil._internal() {
initDio();
}
factory HttpUtil() => _instance;
static HttpUtil getInstance() {
return _instance;
}
/// 对dio进行配置
void initDio() {
KRLogUtil.kr_i('🚀 HttpUtil.initDio() 开始初始化', tag: 'HttpUtil');
// 不使用 Loggy改用自定义简洁拦截器
_dio.interceptors.add(_KRSimpleHttpInterceptor(_dio));
_dio.options.baseUrl = AppConfig.getInstance().baseUrl;
// 设置连接超时时间
_dio.options.connectTimeout = const Duration(seconds: 60);
_dio.options.receiveTimeout = const Duration(seconds: 60);
_dio.options.sendTimeout = const Duration(seconds: 60);
// 设置请求头
_dio.options.headers = {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8',
// 移除固定的UserAgent使用动态的
};
// 设置响应类型
_dio.options.responseType = ResponseType.json;
// 设置验证状态
_dio.options.validateStatus = (status) {
return status != null && status >= 200 && status < 500;
};
// 🔧 配置HttpClientAdapter 优先走本地 sing-box mixed 端口,
// 若代理不可用则回推到直连
KRLogUtil.kr_i('🔧 配置 HttpClientAdapter...', tag: 'HttpUtil');
_dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
useSingBoxProxy: true,
timeout: const Duration(seconds: 10),
);
KRLogUtil.kr_i('✅ HttpUtil.initDio() 初始化完成', tag: 'HttpUtil');
}
/// 更新baseUrl
void updateBaseUrl() {
String newBaseUrl = AppConfig.getInstance().baseUrl;
if (_dio.options.baseUrl != newBaseUrl) {
KRLogUtil.kr_i('🔄 更新baseUrl: ${_dio.options.baseUrl} -> $newBaseUrl',
tag: 'HttpUtil');
_dio.options.baseUrl = newBaseUrl;
}
}
/// 初始化请求头 signature签名字符串
Map<String, dynamic> _initHeader(
String signature, String? userId, String? token) {
Map<String, dynamic> map = <String, dynamic>{};
if (KRAppRunData().kr_isLogin.value == true) {
map["Authorization"] = KRAppRunData().kr_token;
}
// 添加语言请求头
map["lang"] = KRLanguageUtils.getCurrentLanguageCode();
map['Login-Type'] = 'device';
// 添加动态UserAgent头
map["User-Agent"] = _kr_getUserAgent();
return map;
}
/// 获取当前系统的 user_agent
String _kr_getUserAgent() {
if (Platform.isAndroid) {
return 'android';
} else if (Platform.isIOS) {
return 'ios';
} else if (Platform.isMacOS) {
return 'mac';
} else if (Platform.isWindows) {
return 'windows';
} else if (Platform.isLinux) {
return 'linux';
} else if (Platform.isFuchsia) {
return 'harmony';
} else {
return 'unknown';
}
}
/// request请求:T为转换的实体类 path请求地址query请求参数, method: 请求方法, isShowLoading(可选): 是否显示加载中的状态,默认true显示, false为不显示, silentInvite: 是否为静默邀请
Future<BaseResponse<T>> request<T>(String path, Map<String, dynamic> params,
{HttpMethod method = HttpMethod.POST,
bool isShowLoading = true,
bool isSilentInvite = false}) async {
try {
// 每次请求前更新baseUrl确保使用最新的域名
updateBaseUrl();
if (isShowLoading) {
KRCommonUtil.kr_showLoading();
}
var map = <String, dynamic>{};
final plainText = jsonEncode(params);
map = KRAesUtil.encryptData(plainText, AppConfig.kr_encryptionKey);
// 初始化请求头
final headers = _initHeader('signature', 'userId', 'token');
if (isSilentInvite) {
headers['X-Client-Mode'] = 'invite_silent';
}
final options = Options(
contentType: "application/json",
headers: headers,
extra: {'silentInvite': isSilentInvite},
);
Response<Map<String, dynamic>> responseTemp;
if (method == HttpMethod.GET) {
responseTemp = await _dio.get<Map<String, dynamic>>(
path,
queryParameters: map,
options: options,
);
} else if (method == HttpMethod.DELETE) {
responseTemp = await _dio.delete<Map<String, dynamic>>(
path,
data: map,
options: options,
);
} else if (method == HttpMethod.PUT) {
responseTemp = await _dio.put<Map<String, dynamic>>(
path,
data: map,
options: options,
);
} else {
responseTemp = await _dio.post<Map<String, dynamic>>(
path,
data: map,
options: options,
);
}
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
return BaseResponse<T>.fromJson(responseTemp.data!);
} on DioException catch (err) {
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
int code = -90000;
String msg = "";
msg = err.message ?? err.type.toString();
switch (err.type) {
case DioExceptionType.connectionTimeout:
code = -90001;
break;
case DioExceptionType.sendTimeout:
code = -90002;
break;
case DioExceptionType.receiveTimeout:
code = -90003;
break;
case DioExceptionType.badResponse:
code = err.response?.statusCode ?? -90004;
break;
case DioExceptionType.cancel:
break;
case DioExceptionType.connectionError:
code = -90006;
break;
case DioExceptionType.badCertificate:
code = -90007;
break;
default:
if (err.error != null) {
if (err.error.toString().contains("Connection reset by peer")) {
code = -90008;
}
}
}
print('err.type ${err.type}');
if (err.type == DioExceptionType.unknown) {
final _pathOnly = (err.requestOptions.path.isNotEmpty)
? err.requestOptions.path
: err.requestOptions.uri.path;
msg = '${msg.isNotEmpty ? msg : 'unknown'} ($_pathOnly)';
final _ua =
(err.requestOptions.extra['__unknown_attempts'] as int?) ?? 0;
final bool isSilent = err.requestOptions.extra['silentInvite'] ?? false;
if (_ua >= 2 && !isSilent) {
KRCommonUtil.kr_showToast('请求失败($_pathOnly', timeout: 3500);
}
}
return BaseResponse<T>.fromJson(
{'code': code, 'msg': msg, 'data': <String, dynamic>{}});
} catch (e) {
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
return BaseResponse<T>.fromJson({
'code': -90000,
'msg': '${e.toString()} (${path})',
'data': <String, dynamic>{}
});
}
}
}
/// 自定义简洁 HTTP 拦截器(无边框符号)
class _KRSimpleHttpInterceptor extends Interceptor {
/// 常量:手动控制是否打印拦截器日志与解密结果
static const bool KR_HTTP_PRINT = true;
final Dio _dio;
_KRSimpleHttpInterceptor(this._dio);
static String? _lastPath;
static int _lastTsMs = 0;
@override
/// 请求拦截器
///
/// 功能:在调试模式下优先打印请求的解密明文;若解密失败则打印原始数据并标识失败。
/// 参数:
/// - options: 请求选项(包含 data 与 queryParameters
/// - handler: 拦截器处理器
/// 返回void
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (KR_HTTP_PRINT) {
print('>>> Request │ ${options.method}${options.uri}');
}
if (KR_HTTP_PRINT) {
Map<String, dynamic>? m;
if (options.data is Map<String, dynamic>) {
m = options.data as Map<String, dynamic>;
} else if (options.queryParameters.isNotEmpty) {
m = options.queryParameters;
}
if (m != null) {
final decrypted = _tryDecryptFromMap(m);
_printDecryptedOrFallback(
phase: 'Request', raw: m, decrypted: decrypted);
}
}
handler.next(options);
}
@override
/// 响应拦截器
///
/// 功能:在调试模式下优先打印响应的解密明文;若解密失败则打印原始数据并标识失败。
/// 参数:
/// - response: 响应对象(优先识别 Map 格式)
/// - handler: 拦截器处理器
/// 返回void
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (KR_HTTP_PRINT) {
print(
'<<< Response │ ${response.requestOptions.method}${response.statusCode} ${response.statusMessage}${response.requestOptions.uri}');
}
if (KR_HTTP_PRINT && response.data is Map<String, dynamic>) {
final dataMap = response.data as Map<String, dynamic>;
String? decrypted = _tryDecryptFromMap(dataMap);
if (decrypted == null && dataMap['data'] is Map<String, dynamic>) {
decrypted = _tryDecryptFromMap(dataMap['data'] as Map<String, dynamic>);
}
_printDecryptedOrFallback(
phase: 'Response', raw: dataMap, decrypted: decrypted);
}
handler.next(response);
}
@override
/// 错误拦截器
///
/// 功能:打印错误信息并保持原有的 Unknown 错误重试与失败提示去重逻辑。
/// 参数:
/// - err: Dio 错误对象
/// - handler: 拦截器处理器
/// 返回void
void onError(DioException err, ErrorInterceptorHandler handler) {
if (KR_HTTP_PRINT) {
print(
'<<< Error │ ${err.requestOptions.method}${err.requestOptions.uri}');
}
if (KR_HTTP_PRINT) {
print('Error Type: ${err.type}');
}
if (err.message != null) {
if (KR_HTTP_PRINT) {
print('Error Message: ${err.message}');
}
}
if (err.response?.data != null) {
if (KR_HTTP_PRINT) {
print('Response Data: ${err.response?.data}');
}
}
if (err.type == DioExceptionType.unknown) {
final path = (err.requestOptions.path.isNotEmpty)
? err.requestOptions.path
: err.requestOptions.uri.path;
int unknownAttempts =
(err.requestOptions.extra['__unknown_attempts'] as int?) ?? 0;
if (unknownAttempts < 2) {
err.requestOptions.extra['__unknown_attempts'] = unknownAttempts + 1;
final delayMs = unknownAttempts == 0 ? 300 : 700;
Future.delayed(Duration(milliseconds: delayMs)).then((_) async {
final start = DateTime.now();
while (!KRSingBoxImp.instance.kr_isProxyReady &&
DateTime.now().difference(start) < const Duration(seconds: 1)) {
await Future.delayed(const Duration(milliseconds: 100));
}
try {
final Response<dynamic> r =
await _dio.fetch<dynamic>(err.requestOptions);
handler.resolve(r);
} catch (e) {
handler.next(e is DioException
? e
: DioException(requestOptions: err.requestOptions, error: e));
}
});
return;
} else {
final now = DateTime.now().millisecondsSinceEpoch;
final bool isSilent = err.requestOptions.extra['silentInvite'] ?? false;
if (!(_lastPath == path && (now - _lastTsMs) < 2000) && !isSilent) {
_lastPath = path;
_lastTsMs = now;
KRCommonUtil.kr_showToast('请求失败($path', timeout: 3500);
}
KRLogUtil.kr_e('请求失败($path',
tag: 'HttpUtil', error: err, stackTrace: err.stackTrace);
}
}
handler.next(err);
}
/// 尝试从 Map 中解密,若存在 data/time 字段则返回明文,否则返回 null
String? _tryDecryptFromMap(Map<String, dynamic> m) {
final data = m['data'];
final time = m['time'];
if (data is String && time is String) {
try {
return KRAesUtil.decryptData(data, time, AppConfig.kr_encryptionKey);
} catch (_) {
return null;
}
}
return null;
}
/// 将字符串尽量按 JSON 缩进格式化,失败时返回原文
String _prettyPrintJson(String s) {
try {
final jsonData = jsonDecode(s);
return JsonEncoder.withIndent(' ').convert(jsonData);
} catch (_) {
return s;
}
}
/// 统一输出逻辑:成功仅打印解密明文;失败打印失败标识与原文
void _printDecryptedOrFallback({
required String phase,
required Object? raw,
required String? decrypted,
}) {
if (!KR_HTTP_PRINT) return;
if (decrypted != null) {
print('🔓 $phase 明文:');
print(_prettyPrintJson(decrypted));
} else {
print('⚠️ $phase 解密失败');
print('原文:');
if (raw is Map<String, dynamic>) {
try {
print(JsonEncoder.withIndent(' ').convert(raw));
} catch (_) {
print(raw.toString());
}
} else {
print(raw?.toString() ?? 'null');
}
}
}
}