|
|
@ -1,13 +1,15 @@ |
|
|
|
import 'package:dio/dio.dart'; |
|
|
|
import 'package:dio/dio.dart'; |
|
|
|
import 'package:flutter/foundation.dart'; |
|
|
|
import 'package:flutter/foundation.dart'; |
|
|
|
|
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart' hide Response; |
|
|
|
import 'package:get/get.dart' hide Response; |
|
|
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart'; |
|
|
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart'; |
|
|
|
|
|
|
|
import 'package:problem_check_system/app/routes/app_routes.dart'; |
|
|
|
import 'package:problem_check_system/data/repositories/auth_repository.dart'; |
|
|
|
import 'package:problem_check_system/data/repositories/auth_repository.dart'; |
|
|
|
|
|
|
|
|
|
|
|
// DioProvider 是一个 GetxService,确保它在应用生命周期内是单例的。 |
|
|
|
// DioProvider 是一个 GetxService,确保它在应用生命周期内是单例的。 |
|
|
|
// 它负责初始化和配置 Dio 实例,并添加所有拦截器。 |
|
|
|
// 它负责初始化和配置 Dio 实例,并添加所有拦截器。 |
|
|
|
class HttpProvider extends GetxService { |
|
|
|
class HttpProvider extends GetxService { |
|
|
|
static const String _baseUrl = 'https://xh.anxincloud.cn'; |
|
|
|
static const String _baseUrl = 'https://xhdev.anxincloud.cn'; |
|
|
|
|
|
|
|
|
|
|
|
late final Dio _dio; |
|
|
|
late final Dio _dio; |
|
|
|
|
|
|
|
|
|
|
@ -40,72 +42,7 @@ class HttpProvider extends GetxService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
List<Interceptor> _getInterceptors() { |
|
|
|
List<Interceptor> _getInterceptors() { |
|
|
|
return [ |
|
|
|
return [_getErrorInterceptor(), if (kDebugMode) _getLoggerInterceptor()]; |
|
|
|
_getAuthInterceptor(), |
|
|
|
|
|
|
|
_getErrorInterceptor(), |
|
|
|
|
|
|
|
if (kDebugMode) _getLoggerInterceptor(), |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 认证拦截器:处理请求头中的 token 添加和 401 错误重试。 |
|
|
|
|
|
|
|
Interceptor _getAuthInterceptor() { |
|
|
|
|
|
|
|
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 尚未初始化(例如在登录或注册时),则跳过添加认证头。 |
|
|
|
|
|
|
|
debugPrint('AuthRepository 未找到。跳过认证头。'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return handler.next(options); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 在接收到错误时执行 |
|
|
|
|
|
|
|
onError: (error, handler) async { |
|
|
|
|
|
|
|
// 专门处理 401 Unauthorized 错误。 |
|
|
|
|
|
|
|
if (error.response?.statusCode == 401) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
final authRepository = Get.find<AuthRepository>(); |
|
|
|
|
|
|
|
// 尝试刷新 token。 |
|
|
|
|
|
|
|
await authRepository.refreshToken(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果刷新成功,更新请求头并重试原始请求。 |
|
|
|
|
|
|
|
final newOptions = Options( |
|
|
|
|
|
|
|
method: error.requestOptions.method, |
|
|
|
|
|
|
|
headers: error.requestOptions.headers |
|
|
|
|
|
|
|
..['Authorization'] = 'Bearer ${authRepository.getToken()}', |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final response = await _dio.request( |
|
|
|
|
|
|
|
error.requestOptions.path, |
|
|
|
|
|
|
|
data: error.requestOptions.data, |
|
|
|
|
|
|
|
queryParameters: error.requestOptions.queryParameters, |
|
|
|
|
|
|
|
options: newOptions, |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 handler.resolve() 返回重试的结果,阻止错误继续传递。 |
|
|
|
|
|
|
|
return handler.resolve(response); |
|
|
|
|
|
|
|
} on Exception catch (e) { |
|
|
|
|
|
|
|
debugPrint('刷新 token 失败: $e'); |
|
|
|
|
|
|
|
// 如果刷新 token 失败,清除认证数据并跳转到登录页。 |
|
|
|
|
|
|
|
final authRepository = Get.find<AuthRepository>(); |
|
|
|
|
|
|
|
authRepository.clearAuthData(); |
|
|
|
|
|
|
|
if (Get.currentRoute != '/login') { |
|
|
|
|
|
|
|
Get.offAllNamed('/login'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// 传播原始错误,让下一个拦截器(通用错误拦截器)处理。 |
|
|
|
|
|
|
|
return handler.next(error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// 对于所有其他非 401 的错误,将错误传递给下一个拦截器。 |
|
|
|
|
|
|
|
return handler.next(error); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 日志拦截器:在调试模式下打印详细的请求和响应日志。 |
|
|
|
/// 日志拦截器:在调试模式下打印详细的请求和响应日志。 |
|
|
@ -124,20 +61,59 @@ class HttpProvider extends GetxService { |
|
|
|
/// 错误拦截器:处理通用的网络和服务器端错误,并显示 Snackbar 提示。 |
|
|
|
/// 错误拦截器:处理通用的网络和服务器端错误,并显示 Snackbar 提示。 |
|
|
|
Interceptor _getErrorInterceptor() { |
|
|
|
Interceptor _getErrorInterceptor() { |
|
|
|
return InterceptorsWrapper( |
|
|
|
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) { |
|
|
|
onError: (error, handler) { |
|
|
|
// 处理网络连接超时或未知网络错误。 |
|
|
|
// 处理网络连接超时或未知网络错误。 |
|
|
|
if (error.type == DioExceptionType.connectionTimeout || |
|
|
|
if (error.type == DioExceptionType.connectionTimeout || |
|
|
|
error.type == DioExceptionType.receiveTimeout || |
|
|
|
error.type == DioExceptionType.receiveTimeout || |
|
|
|
error.type == DioExceptionType.sendTimeout) { |
|
|
|
error.type == DioExceptionType.sendTimeout) { |
|
|
|
Get.snackbar('网络超时', '请检查网络连接后重试'); |
|
|
|
Get.snackbar( |
|
|
|
|
|
|
|
'网络超时', |
|
|
|
|
|
|
|
'请检查网络连接后重试', |
|
|
|
|
|
|
|
colorText: Colors.white, |
|
|
|
|
|
|
|
backgroundColor: Colors.red, |
|
|
|
|
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
|
|
|
|
); |
|
|
|
} else if (error.type == DioExceptionType.unknown) { |
|
|
|
} else if (error.type == DioExceptionType.unknown) { |
|
|
|
Get.snackbar('网络异常', '请检查网络连接后重试'); |
|
|
|
Get.snackbar( |
|
|
|
|
|
|
|
'网络异常', |
|
|
|
|
|
|
|
'请检查网络连接后重试', |
|
|
|
|
|
|
|
colorText: Colors.white, |
|
|
|
|
|
|
|
backgroundColor: Colors.red, |
|
|
|
|
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 这部分代码只会在 AuthInterceptor 没有处理的错误(即非 401)时执行。 |
|
|
|
// 这部分代码只会在 AuthInterceptor 没有处理的错误(即非 401)时执行。 |
|
|
|
if (error.response != null) { |
|
|
|
if (error.response != null) { |
|
|
|
final message = _handleStatusCode(error.response!); |
|
|
|
final message = _handleStatusCode(error.response!); |
|
|
|
Get.snackbar('请求错误', message); |
|
|
|
Get.snackbar( |
|
|
|
|
|
|
|
'请求错误', |
|
|
|
|
|
|
|
message, |
|
|
|
|
|
|
|
colorText: Colors.white, |
|
|
|
|
|
|
|
backgroundColor: Colors.red, |
|
|
|
|
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return handler.next(error); |
|
|
|
return handler.next(error); |
|
|
@ -151,8 +127,10 @@ class HttpProvider extends GetxService { |
|
|
|
case 400: |
|
|
|
case 400: |
|
|
|
return response.data?['detail'] ?? '请求参数错误'; |
|
|
|
return response.data?['detail'] ?? '请求参数错误'; |
|
|
|
case 401: |
|
|
|
case 401: |
|
|
|
return response.data?['detail'] ?? |
|
|
|
final authRepository = Get.find<AuthRepository>(); |
|
|
|
'未授权,请重新登录'; // 注意:对于 401 错误,这行代码理想情况下不会被执行。 |
|
|
|
authRepository.clearAuthData(); |
|
|
|
|
|
|
|
Get.offAllNamed(AppRoutes.login); |
|
|
|
|
|
|
|
return '未授权,请重新登录'; |
|
|
|
case 403: |
|
|
|
case 403: |
|
|
|
return response.data?['detail'] ?? '访问被拒绝'; |
|
|
|
return response.data?['detail'] ?? '访问被拒绝'; |
|
|
|
case 404: |
|
|
|
case 404: |
|
|
|