base_api.dart 13 KB

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