base_api.dart 13 KB

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