|
|
|
import 'package:dio/dio.dart';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:get/get.dart' hide Response;
|
|
|
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
|
|
|
import 'package:problem_check_system/app/routes/app_routes.dart';
|
|
|
|
import 'package:problem_check_system/core/utils/constants/api_endpoints.dart';
|
|
|
|
import 'package:problem_check_system/data/repositories/auth_repository.dart';
|
|
|
|
|
|
|
|
// DioProvider 是一个 GetxService,确保它在应用生命周期内是单例的。
|
|
|
|
// 它负责初始化和配置 Dio 实例,并添加所有拦截器。
|
|
|
|
class HttpProvider extends GetxService {
|
|
|
|
late final Dio _dio;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> onInit() async {
|
|
|
|
super.onInit();
|
|
|
|
_initDio();
|
|
|
|
}
|
|
|
|
|
|
|
|
// 初始化 Dio 并配置基础选项。
|
|
|
|
void _initDio() {
|
|
|
|
_dio = Dio(
|
|
|
|
BaseOptions(
|
|
|
|
baseUrl: ApiEndpoints.baseUrl,
|
|
|
|
connectTimeout: const Duration(seconds: 30),
|
|
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
|
|
sendTimeout: const Duration(seconds: 30),
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Accept': 'application/json',
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
// 添加拦截器。拦截器的顺序非常关键:
|
|
|
|
// 1. 认证拦截器 (AuthInterceptor): 负责添加 token 和处理 401 错误,优先级最高。
|
|
|
|
// 2. 错误拦截器 (ErrorInterceptor): 处理通用错误,作为所有其他拦截器之后的最终捕获。
|
|
|
|
// 3. 日志拦截器 (LoggerInterceptor): 在调试模式下打印详细日志,方便开发。
|
|
|
|
_dio.interceptors.addAll(_getInterceptors());
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Interceptor> _getInterceptors() {
|
|
|
|
return [_getErrorInterceptor(), if (kDebugMode) _getLoggerInterceptor()];
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 日志拦截器:在调试模式下打印详细的请求和响应日志。
|
|
|
|
Interceptor _getLoggerInterceptor() {
|
|
|
|
return PrettyDioLogger(
|
|
|
|
requestHeader: true,
|
|
|
|
requestBody: true,
|
|
|
|
responseHeader: true,
|
|
|
|
responseBody: true,
|
|
|
|
error: true,
|
|
|
|
compact: false,
|
|
|
|
maxWidth: 90,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 错误拦截器:处理通用的网络和服务器端错误,并显示 Snackbar 提示。
|
|
|
|
Interceptor _getErrorInterceptor() {
|
|
|
|
return InterceptorsWrapper(
|
|
|
|
// 在请求发送前执行
|
|
|
|
onRequest: (options, handler) async {
|
|
|
|
try {
|
|
|
|
// 尝试获取 AuthRepository 并添加 token 到请求头。
|
|
|
|
final authRepository = Get.find<AuthRepository>();
|
|
|
|
final token = authRepository.getToken();
|
|
|
|
if (token != null && token.isNotEmpty) {
|
|
|
|
options.headers['Authorization'] = 'Bearer $token';
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// 如果 AuthRepository 尚未初始化(例如在登录或注册时),则跳过添加认证头。
|
|
|
|
Get.snackbar(
|
|
|
|
'认证过期',
|
|
|
|
'请重新手动登录',
|
|
|
|
colorText: Colors.white,
|
|
|
|
backgroundColor: Colors.red,
|
|
|
|
snackPosition: SnackPosition.TOP,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return handler.next(options);
|
|
|
|
},
|
|
|
|
onError: (error, handler) {
|
|
|
|
// 处理网络连接超时或未知网络错误。
|
|
|
|
if (error.type == DioExceptionType.connectionTimeout ||
|
|
|
|
error.type == DioExceptionType.receiveTimeout ||
|
|
|
|
error.type == DioExceptionType.sendTimeout) {
|
|
|
|
Get.snackbar(
|
|
|
|
'网络超时',
|
|
|
|
'请检查网络连接后重试',
|
|
|
|
colorText: Colors.white,
|
|
|
|
backgroundColor: Colors.red,
|
|
|
|
snackPosition: SnackPosition.TOP,
|
|
|
|
);
|
|
|
|
} else if (error.type == DioExceptionType.unknown) {
|
|
|
|
Get.snackbar(
|
|
|
|
'网络异常',
|
|
|
|
'请检查网络连接后重试',
|
|
|
|
colorText: Colors.white,
|
|
|
|
backgroundColor: Colors.red,
|
|
|
|
snackPosition: SnackPosition.TOP,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 这部分代码只会在 AuthInterceptor 没有处理的错误(即非 401)时执行。
|
|
|
|
if (error.response != null) {
|
|
|
|
final message = _handleStatusCode(error.response!);
|
|
|
|
Get.snackbar(
|
|
|
|
'请求错误',
|
|
|
|
message,
|
|
|
|
colorText: Colors.white,
|
|
|
|
backgroundColor: Colors.red,
|
|
|
|
snackPosition: SnackPosition.TOP,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return handler.next(error);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 辅助方法:根据 HTTP 状态码返回用户友好的错误信息。
|
|
|
|
String _handleStatusCode(Response response) {
|
|
|
|
switch (response.statusCode) {
|
|
|
|
case 400:
|
|
|
|
return response.data?['detail'] ?? '请求参数错误';
|
|
|
|
case 401:
|
|
|
|
final authRepository = Get.find<AuthRepository>();
|
|
|
|
authRepository.clearAuthData();
|
|
|
|
Get.offAllNamed(AppRoutes.login);
|
|
|
|
return '未授权,请重新登录';
|
|
|
|
case 403:
|
|
|
|
return response.data?['detail'] ?? '访问被拒绝';
|
|
|
|
case 404:
|
|
|
|
return response.data?['detail'] ?? '请求资源不存在';
|
|
|
|
case 422:
|
|
|
|
final errors = response.data?['errors'];
|
|
|
|
if (errors != null && errors is Map && errors.isNotEmpty) {
|
|
|
|
return errors.values.first?.first?.toString() ?? '数据验证失败';
|
|
|
|
}
|
|
|
|
return response.data?['detail'] ?? '数据验证失败';
|
|
|
|
case 500:
|
|
|
|
return response.data?['detail'] ?? '服务器内部错误';
|
|
|
|
case 502:
|
|
|
|
return response.data?['detail'] ?? '网关错误';
|
|
|
|
case 503:
|
|
|
|
return response.data?['detail'] ?? '服务不可用';
|
|
|
|
default:
|
|
|
|
return response.data?['detail'] ?? '网络异常(${response.statusCode})';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void clear() {
|
|
|
|
_dio.interceptors.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 新增的请求方法
|
|
|
|
|
|
|
|
/// 发送 GET 请求
|
|
|
|
Future<Response> get(
|
|
|
|
String path, {
|
|
|
|
Map<String, dynamic>? queryParameters,
|
|
|
|
Options? options,
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
ProgressCallback? onReceiveProgress,
|
|
|
|
}) async {
|
|
|
|
return await _dio.get(
|
|
|
|
path,
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
options: options,
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
onReceiveProgress: onReceiveProgress,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 发送 POST 请求
|
|
|
|
Future<Response> post(
|
|
|
|
String path, {
|
|
|
|
dynamic data,
|
|
|
|
Map<String, dynamic>? queryParameters,
|
|
|
|
Options? options,
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
ProgressCallback? onSendProgress,
|
|
|
|
ProgressCallback? onReceiveProgress,
|
|
|
|
}) async {
|
|
|
|
return await _dio.post(
|
|
|
|
path,
|
|
|
|
data: data,
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
options: options,
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
onSendProgress: onSendProgress,
|
|
|
|
onReceiveProgress: onReceiveProgress,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 发送 PUT 请求
|
|
|
|
Future<Response> put(
|
|
|
|
String path, {
|
|
|
|
dynamic data,
|
|
|
|
Map<String, dynamic>? queryParameters,
|
|
|
|
Options? options,
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
ProgressCallback? onSendProgress,
|
|
|
|
ProgressCallback? onReceiveProgress,
|
|
|
|
}) async {
|
|
|
|
return await _dio.put(
|
|
|
|
path,
|
|
|
|
data: data,
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
options: options,
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
onSendProgress: onSendProgress,
|
|
|
|
onReceiveProgress: onReceiveProgress,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 发送 DELETE 请求
|
|
|
|
Future<Response> delete(
|
|
|
|
String path, {
|
|
|
|
dynamic data,
|
|
|
|
Map<String, dynamic>? queryParameters,
|
|
|
|
Options? options,
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
}) async {
|
|
|
|
return await _dio.delete(
|
|
|
|
path,
|
|
|
|
data: data,
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
options: options,
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|