base_api.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. import 'dart:io';
  2. import 'package:dio/dio.dart';
  3. import 'package:dio/io.dart';
  4. import 'package:get/get.dart' as getx;
  5. import '../../../config/translations/strings_enum.dart';
  6. import '../../../utils/api_statistics.dart';
  7. import '../../../utils/log/logger.dart';
  8. import '../../components/country_restricted_overlay.dart';
  9. import '../../data/models/api_result.dart';
  10. enum DomainType {
  11. api, // 普通API域名
  12. log, // 日志上传域名
  13. file, // 文件上传域名
  14. }
  15. /// HTTP请求方法
  16. enum RequestMethod { get, post, put, delete, patch }
  17. /// 重试状态类
  18. class _RetryState {
  19. int currentRetry = 0;
  20. bool isRetrying = false;
  21. void reset() {
  22. currentRetry = 0;
  23. isRetrying = false;
  24. }
  25. }
  26. /// API封装类
  27. abstract class BaseApi {
  28. final CancelToken _cancelToken = CancelToken();
  29. late final Dio dio;
  30. // 不同类型域名的最大重试次数
  31. static const Map<DomainType, int> _maxRetries = {
  32. DomainType.api: 0, // API域名最多重试3次
  33. DomainType.log: 0, // 日志上传最多重试1次
  34. DomainType.file: 0, // 文件上传最多重试1次
  35. };
  36. // 当前重试状态
  37. final Map<DomainType, _RetryState> _retryStates = {
  38. DomainType.api: _RetryState(),
  39. DomainType.log: _RetryState(),
  40. DomainType.file: _RetryState(),
  41. };
  42. String _baseUrl = '';
  43. Map<String, String> _hostIpMap = {}; // 需要绑定的 host -> ip
  44. var _proxy = ''; // 代理配置
  45. /// 设置API基础请求URL
  46. void setbaseUrl(String value, {bool? useProxy}) {
  47. try {
  48. // 参数验证
  49. if (value.isEmpty) {
  50. return;
  51. }
  52. // 解析URL和IP
  53. final urlConfig = _parseUrlConfig(value);
  54. if (urlConfig == null) {
  55. return;
  56. }
  57. final url = urlConfig['url']!;
  58. final ip = urlConfig['ip']!;
  59. // 验证URL格式
  60. if (!_isValidUrl(url)) {
  61. return;
  62. }
  63. if (ip.isNotEmpty) {
  64. // 验证IP格式
  65. if (!_isValidIp(ip)) {
  66. return;
  67. }
  68. // 解析URL获取host
  69. final uri = Uri.parse(url);
  70. if (dio.options.baseUrl == url && _hostIpMap[uri.host] == ip) {
  71. return;
  72. }
  73. _hostIpMap = {uri.host: ip};
  74. _baseUrl = value;
  75. } else {
  76. _hostIpMap = {};
  77. _baseUrl = url;
  78. }
  79. if (dio.options.baseUrl == url) {
  80. return;
  81. }
  82. dio.options.baseUrl = url;
  83. // 更新HttpClient
  84. _updateHttpClient();
  85. } catch (e) {
  86. log('BaseApi', 'setBaseUrl: $e');
  87. }
  88. }
  89. /// 获取API基础请求URL
  90. String get baseUrl => _baseUrl;
  91. /// 设置HTTP代理
  92. ///
  93. /// [proxy]为代理规则,如:PROXY 127.0.0.1:11029
  94. void setProxy(String proxy) {
  95. if (proxy.isNotEmpty) {
  96. _proxy = proxy;
  97. _updateHttpClient();
  98. }
  99. }
  100. void _updateHttpClient() {
  101. dio.httpClientAdapter = IOHttpClientAdapter(
  102. createHttpClient: () {
  103. final client = HttpClient();
  104. // client.findProxy = (uri) => proxy;
  105. client.badCertificateCallback =
  106. (X509Certificate cert, String host, int port) => true;
  107. // 设置其他属性
  108. client.autoUncompress = true;
  109. // 设置用户代理
  110. client.userAgent = 'Nomo/1.0';
  111. // 设置连接工厂
  112. client.connectionFactory = (Uri uri, String? host, int? port) async {
  113. final resolvedHost = host ?? _hostIpMap[uri.host] ?? uri.host;
  114. final actualPort = port ?? uri.port;
  115. if (uri.scheme == 'https') {
  116. // 使用SecureSocket.connect建立真正的SSL连接
  117. return SecureSocket.startConnect(
  118. resolvedHost,
  119. actualPort,
  120. onBadCertificate: (X509Certificate cert) {
  121. return true; // 接受所有证书
  122. },
  123. );
  124. } else {
  125. // 对于HTTP,返回普通Socket连接
  126. return Socket.startConnect(resolvedHost, actualPort);
  127. }
  128. };
  129. client.findProxy = (uri) {
  130. if (_proxy.isNotEmpty) {
  131. return _proxy;
  132. }
  133. return 'DIRECT'; // 不使用代理
  134. };
  135. return client;
  136. },
  137. );
  138. }
  139. /// 解析URL配置字符串
  140. /// 格式: "https://www.google.com:443|ip=54.236.5.237"
  141. Map<String, String>? _parseUrlConfig(String value) {
  142. try {
  143. final parts = value.split('|ip=');
  144. if (parts.length != 2) {
  145. return {'url': value, 'ip': ""};
  146. }
  147. final url = parts[0].trim();
  148. final ip = parts[1].trim();
  149. if (url.isEmpty || ip.isEmpty) {
  150. return null;
  151. }
  152. return {'url': url, 'ip': ip};
  153. } catch (e) {
  154. return null;
  155. }
  156. }
  157. /// 验证URL格式
  158. bool _isValidUrl(String url) {
  159. try {
  160. final uri = Uri.parse(url);
  161. return uri.hasScheme && uri.hasAuthority;
  162. } catch (e) {
  163. return false;
  164. }
  165. }
  166. /// 验证IP地址格式
  167. bool _isValidIp(String ip) {
  168. try {
  169. final parts = ip.split('.');
  170. if (parts.length != 4) return false;
  171. for (final part in parts) {
  172. final num = int.tryParse(part);
  173. if (num == null || num < 0 || num > 255) {
  174. return false;
  175. }
  176. }
  177. return true;
  178. } catch (e) {
  179. return false;
  180. }
  181. }
  182. /// 重置HTTP代理
  183. void resetProxy() {
  184. _proxy = '';
  185. _updateHttpClient();
  186. }
  187. /// 获取默认Header
  188. Map<String, dynamic>? getDefaultHeader() {
  189. return null;
  190. }
  191. /// 获取默认Query
  192. Map<String, dynamic>? getDefaultQuery() {
  193. return null;
  194. }
  195. /// 加密数据
  196. dynamic encrypt(dynamic input) {
  197. return input;
  198. }
  199. /// 解密数据
  200. dynamic decrypt(dynamic input) {
  201. return input;
  202. }
  203. /// 反序列化API返回结果
  204. ApiResult getApiResult(Map<String, dynamic> map) {
  205. return ApiResult(
  206. success: map['success'] ?? false,
  207. data: map['data'],
  208. errorCode: map['errorCode'],
  209. errorMessage: map['errorMessage'],
  210. );
  211. }
  212. // /// 更新基础URL
  213. // void updateBaseUrl(String baseUrl) {
  214. // dio.options.baseUrl = baseUrl;
  215. // log('BaseApi', 'Base URL updated to: $baseUrl');
  216. // }
  217. /// 判断是否应该重试错误
  218. bool _shouldRetryError(DioException error, DomainType domainType) {
  219. final retryState = _retryStates[domainType]!;
  220. // 检查重试次数
  221. if (retryState.currentRetry >= (_maxRetries[domainType] ?? 3)) {
  222. return false;
  223. }
  224. // 网络错误、超时错误、服务器错误(5xx)都应该重试
  225. return checkDioException(error);
  226. }
  227. bool checkDioException(DioException error) {
  228. return error.type == DioExceptionType.connectionTimeout ||
  229. error.type == DioExceptionType.sendTimeout ||
  230. error.type == DioExceptionType.receiveTimeout ||
  231. error.type == DioExceptionType.connectionError ||
  232. (error.response != null &&
  233. error.response!.statusCode != null &&
  234. error.response!.statusCode! >= 500 &&
  235. error.response!.statusCode! < 600) ||
  236. (error.response != null &&
  237. error.response!.statusCode != null &&
  238. error.response!.statusCode! == 404);
  239. }
  240. /// 原始请求方法
  241. Future<ApiResult> rawRequest(
  242. RequestMethod method,
  243. String path, {
  244. dynamic data,
  245. Map<String, dynamic>? header,
  246. Map<String, dynamic>? query,
  247. Options? options,
  248. CancelToken? cancelToken,
  249. DomainType domainType = DomainType.api, // 默认使用API域名
  250. }) async {
  251. final retryState = _retryStates[domainType]!;
  252. log('BaseApi', 'Request: $method ${dio.options.baseUrl}$path');
  253. // 记录请求开始时间
  254. final requestStartTime = DateTime.now().millisecondsSinceEpoch;
  255. try {
  256. // 重置重试状态
  257. if (!retryState.isRetrying) {
  258. retryState.currentRetry = 0;
  259. }
  260. // 合并默认Header和传入的Header
  261. final headers = {...?getDefaultHeader(), ...?header};
  262. // 合并默认Query和传入的Query
  263. final queries = {...?getDefaultQuery(), ...?query};
  264. // 设置请求选项
  265. final requestOptions = options ?? Options();
  266. requestOptions.headers = headers;
  267. // 根据请求方法发送请求
  268. Response response;
  269. switch (method) {
  270. case RequestMethod.get:
  271. response = await dio.get(
  272. path,
  273. queryParameters: queries,
  274. options: requestOptions,
  275. cancelToken: cancelToken,
  276. );
  277. break;
  278. case RequestMethod.post:
  279. response = await dio.post(
  280. path,
  281. data: data,
  282. queryParameters: queries,
  283. options: requestOptions,
  284. cancelToken: cancelToken,
  285. );
  286. break;
  287. case RequestMethod.put:
  288. response = await dio.put(
  289. path,
  290. data: data,
  291. queryParameters: queries,
  292. options: requestOptions,
  293. cancelToken: cancelToken,
  294. );
  295. break;
  296. case RequestMethod.delete:
  297. response = await dio.delete(
  298. path,
  299. data: data,
  300. queryParameters: queries,
  301. options: requestOptions,
  302. cancelToken: cancelToken,
  303. );
  304. break;
  305. case RequestMethod.patch:
  306. response = await dio.patch(
  307. path,
  308. data: data,
  309. queryParameters: queries,
  310. options: requestOptions,
  311. cancelToken: cancelToken,
  312. );
  313. break;
  314. }
  315. // 重置重试状态
  316. retryState.reset();
  317. // 计算响应耗时
  318. final responseTime =
  319. DateTime.now().millisecondsSinceEpoch - requestStartTime;
  320. // 先处理特殊状态码,不进行解密
  321. if (response.statusCode == 204) {
  322. ApiStatistics.instance.addFailure(
  323. domain: _baseUrl,
  324. path: path,
  325. apiRequestTime: requestStartTime,
  326. apiResponseTime: responseTime,
  327. errorCode: response.statusCode ?? 204,
  328. errorMessage: Strings.regionRestricted.tr,
  329. );
  330. getx.Get.offAll(
  331. () => const CountryRestrictedOverlay(),
  332. transition: getx.Transition.fadeIn,
  333. );
  334. return getApiResult({
  335. 'success': false,
  336. 'data': null,
  337. 'errorCode': "${response.statusCode}",
  338. 'errorMessage': Strings.regionRestricted.tr,
  339. });
  340. }
  341. // 正常状态码才进行解密
  342. if (response.statusCode == 200) {
  343. final result = getApiResult(decrypt(response.data));
  344. // 记录统计
  345. if (result.success) {
  346. ApiStatistics.instance.addSuccess(
  347. domain: _baseUrl,
  348. path: path,
  349. apiRequestTime: requestStartTime,
  350. apiResponseTime: responseTime,
  351. errorCode: response.statusCode ?? 200,
  352. );
  353. } else {
  354. ApiStatistics.instance.addFailure(
  355. domain: _baseUrl,
  356. path: path,
  357. apiRequestTime: requestStartTime,
  358. apiResponseTime: responseTime,
  359. errorCode: int.tryParse(result.errorCode ?? '0') ?? 0,
  360. errorMessage: result.errorMessage ?? '',
  361. );
  362. }
  363. return result;
  364. }
  365. // 其他状态码返回原始数据
  366. final result = getApiResult(response.data);
  367. // 记录失败统计
  368. ApiStatistics.instance.addFailure(
  369. domain: _baseUrl,
  370. path: path,
  371. apiRequestTime: requestStartTime,
  372. apiResponseTime: responseTime,
  373. errorCode: response.statusCode ?? 0,
  374. errorMessage: result.errorMessage ?? 'Unknown error',
  375. );
  376. return result;
  377. } on DioException catch (e) {
  378. // 检查是否应该重试
  379. if (_shouldRetryError(e, domainType)) {
  380. retryState.isRetrying = true;
  381. retryState.currentRetry++;
  382. log(
  383. 'BaseApi',
  384. 'Request failed (${domainType.name}). Retrying (${retryState.currentRetry}/${_maxRetries[domainType]})...',
  385. );
  386. // 重试请求
  387. return rawRequest(
  388. method,
  389. path,
  390. data: data,
  391. header: header,
  392. query: query,
  393. options: options,
  394. cancelToken: cancelToken,
  395. domainType: domainType,
  396. );
  397. }
  398. // 计算响应耗时
  399. final responseTime =
  400. DateTime.now().millisecondsSinceEpoch - requestStartTime;
  401. // 记录失败统计
  402. ApiStatistics.instance.addFailure(
  403. domain: _baseUrl,
  404. path: path,
  405. apiRequestTime: requestStartTime,
  406. apiResponseTime: responseTime,
  407. errorCode: e.response?.statusCode ?? -1,
  408. errorMessage: e.message ?? e.type.name,
  409. );
  410. // 重置重试状态
  411. retryState.reset();
  412. rethrow;
  413. } catch (e) {
  414. // 重置重试状态
  415. retryState.reset();
  416. rethrow;
  417. }
  418. }
  419. /// GET请求
  420. Future<dynamic> get(
  421. String path, {
  422. Map<String, dynamic>? query,
  423. Map<String, dynamic>? header,
  424. Options? options,
  425. CancelToken? cancelToken,
  426. DomainType domainType = DomainType.api,
  427. }) async {
  428. return rawRequest(
  429. RequestMethod.get,
  430. path,
  431. query: query,
  432. header: header,
  433. options: options,
  434. cancelToken: cancelToken,
  435. domainType: domainType,
  436. );
  437. }
  438. /// POST请求
  439. Future<ApiResult> post(
  440. String path, {
  441. dynamic data,
  442. Map<String, dynamic>? query,
  443. Map<String, dynamic>? header,
  444. Options? options,
  445. CancelToken? cancelToken,
  446. DomainType domainType = DomainType.api,
  447. }) async {
  448. // 加密数据
  449. final encryptedData = encrypt(data);
  450. return rawRequest(
  451. RequestMethod.post,
  452. path,
  453. data: encryptedData,
  454. query: query,
  455. header: header,
  456. options: options,
  457. cancelToken: cancelToken,
  458. domainType: domainType,
  459. );
  460. }
  461. /// PUT请求
  462. Future<dynamic> put(
  463. String path, {
  464. dynamic data,
  465. Map<String, dynamic>? query,
  466. Map<String, dynamic>? header,
  467. Options? options,
  468. CancelToken? cancelToken,
  469. DomainType domainType = DomainType.api,
  470. }) async {
  471. // 加密数据
  472. final encryptedData = encrypt(data);
  473. return rawRequest(
  474. RequestMethod.put,
  475. path,
  476. data: encryptedData,
  477. query: query,
  478. header: header,
  479. options: options,
  480. cancelToken: cancelToken,
  481. domainType: domainType,
  482. );
  483. }
  484. /// DELETE请求
  485. Future<dynamic> delete(
  486. String path, {
  487. dynamic data,
  488. Map<String, dynamic>? query,
  489. Map<String, dynamic>? header,
  490. Options? options,
  491. CancelToken? cancelToken,
  492. DomainType domainType = DomainType.api,
  493. }) async {
  494. // 加密数据
  495. final encryptedData = encrypt(data);
  496. return rawRequest(
  497. RequestMethod.delete,
  498. path,
  499. data: encryptedData,
  500. query: query,
  501. header: header,
  502. options: options,
  503. cancelToken: cancelToken,
  504. domainType: domainType,
  505. );
  506. }
  507. /// PATCH请求
  508. Future<dynamic> patch(
  509. String path, {
  510. dynamic data,
  511. Map<String, dynamic>? query,
  512. Map<String, dynamic>? header,
  513. Options? options,
  514. CancelToken? cancelToken,
  515. DomainType domainType = DomainType.api,
  516. }) async {
  517. // 加密数据
  518. final encryptedData = encrypt(data);
  519. return rawRequest(
  520. RequestMethod.patch,
  521. path,
  522. data: encryptedData,
  523. query: query,
  524. header: header,
  525. options: options,
  526. cancelToken: cancelToken,
  527. domainType: domainType,
  528. );
  529. }
  530. /// 取消所有未返回的请求
  531. void cancelRequests() {
  532. _cancelToken.cancel("cancelled");
  533. }
  534. }