You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

172 lines
6.1 KiB

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 'api_crypto.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<VersionUpdateService>()) {
// 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());
// 解密拦截器必须排在 AuthInterceptor 之前:加密接口(user_getappconfig_v3 等)的 body 是 base64 密文,
// AuthInterceptor 一看到不是业务 JSON 就当失败。见 api_crypto.dart。
dio.interceptors.add(ApiDecryptInterceptor());
dio.interceptors.add(AuthInterceptor());
}
// 请求,返回参数为 T
// method:请求方法,NWMethod.POST等
// path:请求地址
// params:请求参数
/// [timeout] 覆盖本次请求的收发超时(默认走全局的 30s)。
///
/// ⚠️ 实时链路**必须**自己给一个短超时:全局那 30s 是给上传、总结这类慢请求留的,
/// 而同传/面对面是流式的,一句话等 30s 早就没有意义了,只会让后面的句子一起堵住,
/// 表现就是「弱网下卡住不动」。服务端 user_translate 内部的上下文只给 10s,
/// 客户端再等 30s 纯属白等 20s。
Future request<T>(
NWMethod method,
String path, {
Object? params,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Duration? timeout,
}) async {
Response response = await dio.request(
path,
data: params,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: Options(
method: nwMethodValues[method],
sendTimeout: timeout,
receiveTimeout: timeout,
),
);
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);
}
}