339 lines
9.8 KiB
Dart
339 lines
9.8 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flustars/flustars.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get/get.dart' hide Response;
|
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
|
|
|
import '../kt_pages/kt_routes.dart';
|
|
import '../kt_utils/kt_keys.dart';
|
|
import '../kt_utils/kt_toast_utils.dart';
|
|
import '../kt_utils/kt_user_utils.dart';
|
|
import 'kt_apis.dart';
|
|
import 'kt_request_interceptor.dart';
|
|
|
|
/// HTTP请求配置类
|
|
class HttpConfig {
|
|
static const String baseUrl = KtApis.baseUrl;
|
|
static const int connectTimeout = 5000; // 连接超时时间(ms)
|
|
static const int receiveTimeout = 15000; // 接收超时时间(ms)
|
|
static const int sendTimeout = 5000; // 发送超时时间(ms)
|
|
}
|
|
|
|
/// API响应结果类
|
|
class ApiResponse<T> {
|
|
final bool success;
|
|
final T? data;
|
|
final String? message;
|
|
final int? code;
|
|
|
|
ApiResponse({required this.success, this.data, this.message, this.code});
|
|
|
|
factory ApiResponse.fromJson(Map<String, dynamic> json) {
|
|
return ApiResponse(
|
|
success: json['success'] ?? false,
|
|
data: json['data'],
|
|
message: json['msg'],
|
|
code: json['code'],
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {'success': success, 'data': data, 'message': message, 'code': code};
|
|
}
|
|
}
|
|
|
|
/// HTTP请求方法枚举
|
|
enum HttpMethod { get, post, put, delete, patch }
|
|
|
|
/// 统一的API异常类
|
|
class ApiException implements Exception {
|
|
final int? code;
|
|
final String message;
|
|
final dynamic error;
|
|
|
|
ApiException({this.code, required this.message, this.error});
|
|
|
|
@override
|
|
String toString() {
|
|
return 'ApiException{code: $code, message: $message, error: $error}';
|
|
}
|
|
}
|
|
|
|
/// Dio请求封装类
|
|
class KtHttpClient {
|
|
static final KtHttpClient _instance = KtHttpClient._internal();
|
|
|
|
factory KtHttpClient() => _instance;
|
|
late Dio _dio;
|
|
|
|
KtHttpClient._internal() {
|
|
_initDio();
|
|
}
|
|
|
|
void _initDio() {
|
|
BaseOptions options = BaseOptions(
|
|
baseUrl: HttpConfig.baseUrl,
|
|
connectTimeout: Duration(seconds: HttpConfig.connectTimeout),
|
|
receiveTimeout: Duration(seconds: HttpConfig.receiveTimeout),
|
|
sendTimeout: Duration(seconds: HttpConfig.sendTimeout),
|
|
responseType: ResponseType.json,
|
|
);
|
|
|
|
_dio = Dio(options);
|
|
|
|
_dio.interceptors.add(RequestInterceptor());
|
|
// 添加拦截器
|
|
_dio.interceptors.add(
|
|
InterceptorsWrapper(
|
|
onRequest: (options, handler) {
|
|
return handler.next(options);
|
|
},
|
|
onResponse: (response, handler) {
|
|
return handler.next(response);
|
|
},
|
|
onError: (DioException e, handler) {
|
|
if (kDebugMode) {
|
|
print('请求错误: ${e.message}');
|
|
if (e.response != null) {
|
|
print('错误响应: ${e.response!.data}');
|
|
}
|
|
}
|
|
return handler.next(_handleError(e));
|
|
},
|
|
),
|
|
);
|
|
_dio.interceptors.add(
|
|
PrettyDioLogger(requestBody: true, requestHeader: true),
|
|
);
|
|
}
|
|
|
|
/// 处理Dio错误
|
|
DioException _handleError(DioException e) {
|
|
switch (e.type) {
|
|
case DioExceptionType.connectionTimeout:
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(code: -1, message: 'Connection timeout'),
|
|
);
|
|
case DioExceptionType.sendTimeout:
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(code: -2, message: 'Connection timeout'),
|
|
);
|
|
case DioExceptionType.receiveTimeout:
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(code: -3, message: 'Connection timeout'),
|
|
);
|
|
case DioExceptionType.badResponse:
|
|
final int? statusCode = e.response?.statusCode;
|
|
String message = 'Connection failed';
|
|
|
|
if (statusCode != null) {
|
|
switch (statusCode) {
|
|
case 400:
|
|
message = '请求参数错误';
|
|
break;
|
|
case 401:
|
|
message = 'Token已过期';
|
|
SpUtil.remove(KtKeys.token);
|
|
clearAuthToken();
|
|
Get.offAllNamed(KtRoutes.splash);
|
|
break;
|
|
case 402:
|
|
message = '未授权,请先登录';
|
|
// ToastUtils.show(
|
|
// 'Please log out of the account on other devices in time, otherwise the App on other devices will be unavailable.',
|
|
// );
|
|
KtUserUtil().register(toHome: false);
|
|
|
|
// SpUtil.remove(SpKeys.token);
|
|
// clearAuthToken();
|
|
// Get.offAllNamed(AppRoutes.splash);
|
|
break;
|
|
case 403:
|
|
message = '禁止访问,权限不足';
|
|
break;
|
|
case 404:
|
|
message = '请求资源不存在';
|
|
break;
|
|
case 500:
|
|
message = '服务器内部错误';
|
|
break;
|
|
case 502:
|
|
message = '网关错误';
|
|
break;
|
|
case 503:
|
|
message = '服务不可用';
|
|
break;
|
|
case 504:
|
|
message = '网关超时';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(
|
|
code: statusCode ?? -4,
|
|
message: message,
|
|
error: e.response?.data,
|
|
),
|
|
);
|
|
case DioExceptionType.cancel:
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(code: -5, message: '请求已取消'),
|
|
);
|
|
case DioExceptionType.unknown:
|
|
return DioException(
|
|
requestOptions: e.requestOptions,
|
|
error: ApiException(code: -6, message: '未知错误,请稍后再试', error: e.error),
|
|
);
|
|
default:
|
|
return e;
|
|
}
|
|
}
|
|
|
|
/// 统一的请求方法
|
|
Future<ApiResponse<T>> request<T>(
|
|
String path, {
|
|
HttpMethod method = HttpMethod.post,
|
|
dynamic data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
ProgressCallback? onSendProgress,
|
|
ProgressCallback? onReceiveProgress,
|
|
T Function(dynamic data)? parser,
|
|
bool showErrorToast = false,
|
|
}) async {
|
|
try {
|
|
Options requestOptions = options ?? Options();
|
|
requestOptions.method = method.name.toUpperCase();
|
|
|
|
Response response;
|
|
|
|
switch (method) {
|
|
case HttpMethod.get:
|
|
response = await _dio.get(
|
|
path,
|
|
queryParameters: queryParameters,
|
|
options: requestOptions,
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
break;
|
|
case HttpMethod.post:
|
|
response = await _dio.post(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: requestOptions,
|
|
onSendProgress: onSendProgress,
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
break;
|
|
case HttpMethod.put:
|
|
response = await _dio.put(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: requestOptions,
|
|
);
|
|
break;
|
|
case HttpMethod.delete:
|
|
response = await _dio.delete(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: requestOptions,
|
|
);
|
|
break;
|
|
case HttpMethod.patch:
|
|
response = await _dio.patch(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: requestOptions,
|
|
);
|
|
break;
|
|
}
|
|
|
|
// 解析响应数据
|
|
if (response.statusCode == 200) {
|
|
final responseData = response.data;
|
|
|
|
if (responseData['code'] == 200) {
|
|
// 如果有自定义解析器,使用解析器处理数据
|
|
if (parser != null) {
|
|
return ApiResponse(success: true, data: parser(responseData));
|
|
}
|
|
|
|
// 默认解析
|
|
if (responseData is Map<String, dynamic>) {
|
|
responseData.putIfAbsent('success', () => true);
|
|
return ApiResponse.fromJson(responseData);
|
|
} else {
|
|
return ApiResponse(success: true, data: responseData as T?);
|
|
}
|
|
} else {
|
|
if (showErrorToast)
|
|
KtToastUtils.showSuccess(placeholder: responseData['msg']);
|
|
return ApiResponse(
|
|
success: false,
|
|
message: responseData['msg'],
|
|
data: responseData as T?,
|
|
);
|
|
}
|
|
} else {
|
|
throw ApiException(
|
|
code: response.statusCode,
|
|
message: '请求失败,状态码: ${response.statusCode}',
|
|
error: response.data,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (e is ApiException) {
|
|
rethrow;
|
|
} else {
|
|
throw ApiException(code: -1000, message: '请求发生异常: $e', error: e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 设置认证令牌
|
|
void setAuthToken(String token) {
|
|
_dio.options.headers['Authorization'] = token;
|
|
// _dio.options.headers['Authorization'] = 'Bearer $token';
|
|
}
|
|
|
|
/// 清除认证令牌
|
|
void clearAuthToken() {
|
|
_dio.options.headers.remove('Authorization');
|
|
}
|
|
|
|
/// 添加自定义请求头字段
|
|
void addHeader(String key, String value) {
|
|
_dio.options.headers[key] = value;
|
|
}
|
|
|
|
/// 批量添加自定义请求头字段
|
|
void addHeaders(Map<String, String> headers) {
|
|
headers.forEach((key, value) => _dio.options.headers[key] = value);
|
|
}
|
|
|
|
/// 移除指定请求头字段
|
|
void removeHeader(String key) {
|
|
_dio.options.headers.remove(key);
|
|
}
|
|
|
|
/// 清除所有自定义请求头字段
|
|
void clearHeaders() {
|
|
// 保留基础的Content-Type
|
|
final contentType = _dio.options.headers['Content-Type'];
|
|
_dio.options.headers.clear();
|
|
if (contentType != null) {
|
|
_dio.options.headers['Content-Type'] = contentType;
|
|
}
|
|
}
|
|
}
|