import 'dart:convert'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:hgdj/config/config.dart'; import 'package:hgdj/hj_utils/text_util.dart'; import 'package:hgdj/tools_base/debug_log.dart'; import 'package:hgdj/tools_base/net/base_resp_bean.dart'; import 'package:hgdj/tools_base/net/http_response_interceptor.dart'; import 'package:hgdj/tools_base/net/net_code.dart'; import 'package:hgdj/tools_base/net/net_manager.dart'; import 'package:uuid/uuid.dart'; import '../../track_event_manager/device_service.dart'; import '../../track_event_manager/track_session.dart'; final httpManager = _HttpManagerImp(); enum HttpMethod { get('GET'), post('POST'), delete('DELETE'); final String method; const HttpMethod(this.method); } abstract class HttpManger { final dio = Dio(); String _baseUrl = ''; String get baseUrl => _baseUrl; /// 服务器时间校准 int _diffTimeInSeconds = 0; DateTime? _serverTime; initDefault() { // 选线发生在 resetBaseUrl 之前,这里必须先给 dio.options 配 connectTimeout, // 否则线路握手挂起时永不超时,导致启动页"选线中..."一直转(iOS release 高发) dio.options.connectTimeout = const Duration(seconds: 15); dio.options.sendTimeout = const Duration(seconds: 15); dio.options.receiveTimeout = const Duration(seconds: 15); _addDioIns(); } init(String baseUrl) { resetBaseUrl(baseUrl); } // 同步服务器时间 setServerTime(String? serverTimeS) { if (TextUtil.isNotEmpty(serverTimeS)) { _serverTime = DateTime.parse(serverTimeS!); _diffTimeInSeconds = DateTime.now().difference(_serverTime!).inSeconds; debugLog( "============>server diff from local in seconds:$_diffTimeInSeconds"); } } /// get Future fetchResponseByGET( String url, { Map? param, Options? options, Function(Map json)? jsonTransformation, }); /// post Future fetchResponseByPOST( String url, { Map? param, Options? options, Function(Map json)? jsonTransformation, }); /// post Future fetchResponseByDELETE( String url, { Map? param, Options? options, Function(Map json)? jsonTransformation, }); Future _requestByUrl( String url, { Map? data, Map? queryParameters, required Options options, CancelToken? cancelToken, Function(Map json)? jsonTransformation, }) async { // 给默认值兜底:若 response 非 null 但 data 不是 Map/BaseRespBean(null/String/List), // 下面 resultData 不会被赋值,late 变量访问会抛 LateInitializationError BaseRespBean resultData = BaseRespBean(Code.NETWORK_ERROR, msg: '网络异常请稍后再试~', data: null); Response? response; try { response = await dio.request(url, queryParameters: queryParameters, options: options, data: data, cancelToken: cancelToken); } on DioException catch (e) { debugLog('DioException $e'); if (e.type == DioExceptionType.cancel) { resultData = BaseRespBean(Code.LOCAL_CANCEL_REQUEST, msg: '请求已经取消~,请重试', data: null); } else if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.sendTimeout) { resultData = BaseRespBean(Code.NETWORK_TIMEOUT, msg: '网络连接超时~', data: null); } final connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult == ConnectivityResult.none) { //没有网络 resultData = BaseRespBean(Code.LOCAL_NO_NETWORK, msg: '暂无网络,请检查网络设置', data: null); } else { resultData = BaseRespBean(Code.NETWORK_ERROR, msg: '网络异常,请重新试试~', data: null); } } on SocketException catch (e) { resultData = BaseRespBean(Code.NETWORK_TIMEOUT, msg: '网络连接超时~', data: null); debugLog('SocketException $e'); } on HttpException catch (e) { resultData = BaseRespBean(Code.NETWORK_ERROR, msg: '网络异常请稍后再试~', data: null); debugLog('HttpException $e'); } on FormatException catch (e) { debugLog('FormatException $e'); resultData = BaseRespBean(Code.NETWORK_ERROR, msg: '网络异常请稍后再试~', data: null); } catch (e) { resultData = BaseRespBean(Code.NETWORK_ERROR, msg: '网络异常请稍后再试~', data: null); debugLog(e); } if (response == null) { return resultData; } else if (response.data is BaseRespBean) { resultData = response.data; } else if (response.data is Map) { resultData = BaseRespBean.fromJson(response.data); } if (resultData.isSuccess) { if (jsonTransformation != null) { final data_ = resultData.data; if (data_ is Map) { try { resultData.data = jsonTransformation.call(Map.from(data_)); } catch (e) { print('jsonTransformation error $e'); resultData.data = null; } } } } else if (resultData.data is String) { // 非成功响应 data 仍是未解密的密文串;service 层若 `return result.data`(返回类型是 Model?) // 会把 String 强转模型,抛 'String is not a subtype of FutureOr'。统一置空,降级返回 null。 resultData.data = null; } return resultData; } // dio 添加拦截 _addDioIns() { dio.interceptors.add(HttpResponseInterceptor()); } // 重制地址 resetBaseUrl(String baseUrl) { _baseUrl = baseUrl; final options = BaseOptions( connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), validateStatus: (int? status) => (status ?? 600) < 600, baseUrl: baseUrl, ); options.headers[HttpHeaders.acceptEncodingHeader] = "*"; dio.options = options; var adapter = DefaultHttpClientAdapter(); adapter.onHttpClientCreate = (client) { client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; return client; }; dio.httpClientAdapter = adapter; } // 获取统一的请求头 Future generateRequestOption(String apiUrl, {Options? options, required HttpMethod method}) async { //调用方可以自带 options(比如 X-Request-ID 这种单接口的头),公共头往上加,method 一律以本次请求为准 options ??= Options(); options.method = method.method; options.headers ??= {}; final token = await netManager.getToken(); if (token.isNotEmpty == true) { options.headers?["Authorization"] = token; } if (options.method == "GET") { //options.headers["Content-Type"] = "application/x-www-form-urlencoded"; } else if (options.method == "POST") { options.headers?["Content-Type"] = "application/json;charset=UTF-8"; } options.headers?["User-Agent"] = await netManager.userAgent(); options.headers?["api_version"] = "1.0.0"; options.headers?["device"] = Platform.operatingSystem; Uri? baseUri = Uri.tryParse(baseUrl); Uri targetUri = Uri( scheme: baseUri?.scheme, host: baseUri?.host, port: baseUri?.port, path: baseUri!.path + apiUrl); options.headers?["x-api-key"] = await _sign(targetUri.path); options.headers?["sid"] = TrackSessionManager().currentSid; options.headers?["DeviceModel"] = DeviceInfoService.model; return options; } /// 签名 Future _sign(String path) async { Map signObj = {}; final timeDate = DateTime.now().add(Duration(seconds: -_diffTimeInSeconds)); int timestamp = timeDate.toUtc().millisecondsSinceEpoch ~/ 1000; signObj['nonce'] = const Uuid().v4(); signObj['path'] = path; signObj['timestamp'] = timestamp.toString(); signObj['token'] = await netManager.getToken(); signObj['userAgent'] = await netManager.userAgent(); var key = utf8.encode(Config.signKey); var bytes = utf8.encode(jsonEncode(signObj).toString()); var sha1Encrypt = Hmac(sha1, key); var digest = sha1Encrypt.convert(bytes); return 'timestamp=$timestamp;sign=${digest.toString()};nonce=${signObj['nonce']}'; } } class _HttpManagerImp extends HttpManger { @override Future fetchResponseByDELETE(String url, {Map? param, Options? options, Function(Map json)? jsonTransformation}) async { final reqOptions = await generateRequestOption(url, options: options, method: HttpMethod.delete); return await _requestByUrl(url, options: reqOptions, data: param, jsonTransformation: jsonTransformation); } @override Future fetchResponseByGET( String url, { Map? param, Options? options, Function(Map json)? jsonTransformation, }) async { final reqOptions = await generateRequestOption(url, options: options, method: HttpMethod.get); return await _requestByUrl(url, options: reqOptions, queryParameters: param, jsonTransformation: jsonTransformation); } @override Future fetchResponseByPOST( String url, { Map? param, Options? options, Function(Map json)? jsonTransformation, }) async { final reqOptions = await generateRequestOption(url, options: options, method: HttpMethod.post); return await _requestByUrl(url, options: reqOptions, data: param, jsonTransformation: jsonTransformation); } Future fetchDetectLineResponse(String url, {Options? options, CancelToken? cancelToken}) async { options ??= Options( method: HttpMethod.get.method, sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: {}, ); final result = await _requestByUrl(url, options: options, cancelToken: cancelToken); return result; } }