Files
2026-09-15 15:44:13 +07:00

309 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<BaseRespBean> fetchResponseByGET(
String url, {
Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> json)? jsonTransformation,
});
/// post
Future<BaseRespBean> fetchResponseByPOST(
String url, {
Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> json)? jsonTransformation,
});
/// post
Future<BaseRespBean> fetchResponseByDELETE(
String url, {
Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> json)? jsonTransformation,
});
Future<BaseRespBean> _requestByUrl(
String url, {
Map<String, dynamic>? data,
Map<String, dynamic>? queryParameters,
required Options options,
CancelToken? cancelToken,
Function(Map<String, dynamic> json)? jsonTransformation,
}) async {
// 给默认值兜底:若 response 非 null 但 data 不是 Map/BaseRespBeannull/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<String, dynamic>) {
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<String, dynamic>.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<Model?>'。统一置空,降级返回 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<Options> 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<String> _sign(String path) async {
Map<String, dynamic> 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<BaseRespBean> fetchResponseByDELETE(String url,
{Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> 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<BaseRespBean> fetchResponseByGET(
String url, {
Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> 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<BaseRespBean> fetchResponseByPOST(
String url, {
Map<String, dynamic>? param,
Options? options,
Function(Map<String, dynamic> json)? jsonTransformation,
}) async {
final reqOptions = await generateRequestOption(url,
options: options, method: HttpMethod.post);
return await _requestByUrl(url,
options: reqOptions,
data: param,
jsonTransformation: jsonTransformation);
}
Future<BaseRespBean> 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;
}
}