import 'dart:io'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get/get.dart' hide Response, FormData; import '../../../core/utils/logger.dart'; import '../../models/channel_app_model.dart'; import '../version_update_service.dart'; import 'auth_interceptor.dart'; import 'nw_method.dart'; class DioManager { static final DioManager _shared = DioManager._internal(); factory DioManager() => _shared; late Dio dio; // 获取当前渠道信息,优先从版本服务获取,降级到平台判断和.env UserChannel get _currentChannel { try { if (Get.isRegistered()) { // print("lxm---${VersionUpdateService.to.cachedChannel}"); return VersionUpdateService.to.cachedChannel; } } catch (_) {} // 版本服务未就绪时,根据平台判断 if (Platform.isIOS) { return UserChannel.GLOBAL_APPLE; } final channelName = dotenv.env['Channel'] ?? 'OFFICIAL'; return UserChannel.fromName(channelName); } // 根据地区获取服务器URL // // HW_SERVER_URL 一旦在 .env 里留空,非中国大陆地区/GOOGLE 渠道的用户就会 // 拿到空字符串当 baseUrl——Dio 请求全部变成无 host 的相对路径,静默失败, // 隐私政策页面空白、登录连不上后端,什么报错都看不出真实原因。这里兜底 // 退回 SERVER_URL,避免一个环境变量漏填拖垮整条网络链路。 String get baseUrl { final countryCode = Get.deviceLocale?.countryCode?.toUpperCase(); final isChinaMainland = countryCode == 'CN'; final channel = _currentChannel; final serverUrl = dotenv.env['SERVER_URL'] ?? ''; if (!isChinaMainland || channel == UserChannel.GLOBAL_GOOGLE) { final hwServerUrl = dotenv.env['HW_SERVER_URL'] ?? ''; return hwServerUrl.isNotEmpty ? hwServerUrl : serverUrl; } else { return serverUrl; } } DioManager._internal() { BaseOptions options = BaseOptions( baseUrl: baseUrl, contentType: Headers.jsonContentType, responseType: ResponseType.json, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), ); dio = Dio(options); // 日志拦截器必须在 AuthInterceptor 前面, // 否则 AuthInterceptor 的 handler.resolve() 会跳过后续拦截器 dio.interceptors.add(_ApiLogInterceptor()); dio.interceptors.add(AuthInterceptor()); } // 请求,返回参数为 T // method:请求方法,NWMethod.POST等 // path:请求地址 // params:请求参数 Future request( NWMethod method, String path, { Object? params, Map? queryParameters, CancelToken? cancelToken, }) async { Response response = await dio.request( path, data: params, queryParameters: queryParameters, cancelToken: cancelToken, options: Options(method: nwMethodValues[method]), ); return response.data; } } /// API 请求/响应日志拦截器,写入文件日志 /// 必须放在 AuthInterceptor 之前,才能完整记录原始响应 class _ApiLogInterceptor extends Interceptor { static const String _tag = 'API'; String _formatBody(dynamic data) { if (data == null) return 'null'; try { if (data is FormData) { final fields = data.fields.map((e) => '${e.key}=${e.value}').join(', '); final files = data.files.map((e) => '${e.key}=${e.value.filename}').join(', '); return 'FormData{fields: [$fields], files: [$files]}'; } if (data is Map || data is List) { return const JsonEncoder().convert(data); } return data.toString(); } catch (_) { return data.toString(); } } @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final method = options.method; final path = options.path; final buf = StringBuffer(); buf.writeln('>>> $method $path'); if (options.queryParameters.isNotEmpty) { buf.writeln(' Query: ${_formatBody(options.queryParameters)}'); } if (options.data != null) { buf.writeln(' Body: ${_formatBody(options.data)}'); } Logger.i(_tag, buf.toString().trimRight()); handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { final method = response.requestOptions.method; final path = response.requestOptions.path; final statusCode = response.statusCode; final buf = StringBuffer(); buf.writeln('<<< $method $path [$statusCode]'); buf.writeln(' Response: ${_formatBody(response.data)}'); Logger.i(_tag, buf.toString().trimRight()); handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) { final method = err.requestOptions.method; final path = err.requestOptions.path; final statusCode = err.response?.statusCode ?? 'N/A'; final buf = StringBuffer(); buf.writeln('<<< $method $path [$statusCode] ${err.type}: ${err.message}'); if (err.response?.data != null) { buf.writeln(' Error Body: ${_formatBody(err.response?.data)}'); } Logger.e(_tag, buf.toString().trimRight()); handler.next(err); } }