You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
8.8 KiB
266 lines
8.8 KiB
2 weeks ago
|
import 'package:dio/dio.dart';
|
||
|
import 'package:flutter/foundation.dart';
|
||
|
import 'package:get/get.dart' hide Response;
|
||
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||
|
import 'package:problem_check_system/data/repositories/auth_repository.dart';
|
||
|
import 'package:problem_check_system/modules/auth/controllers/auth_controller.dart';
|
||
|
|
||
|
// DioProvider 是一个 GetxService,确保它在应用生命周期内是单例的。
|
||
|
// 它负责初始化和配置 Dio 实例,并添加所有拦截器。
|
||
|
class DioProvider extends GetxService {
|
||
|
static const String _baseUrl = 'https://xh.anxincloud.cn';
|
||
|
|
||
|
late final Dio _dio;
|
||
|
|
||
|
@override
|
||
|
Future<void> onInit() async {
|
||
|
super.onInit();
|
||
|
_initDio();
|
||
|
}
|
||
|
|
||
|
// 初始化 Dio 并配置基础选项。
|
||
|
void _initDio() {
|
||
|
_dio = Dio(
|
||
|
BaseOptions(
|
||
|
baseUrl: _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 [
|
||
|
_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 失败,清除认证数据并跳转到登录页。
|
||
|
try {
|
||
|
final authController = Get.find<AuthController>();
|
||
|
await authController.logout();
|
||
|
} catch (e) {
|
||
|
// 如果 AuthController 找不到,作为备用方案手动清除数据并导航。
|
||
|
final authRepository = Get.find<AuthRepository>();
|
||
|
authRepository.clearAuthData();
|
||
|
if (Get.currentRoute != '/login') {
|
||
|
Get.offAllNamed('/login');
|
||
|
}
|
||
|
}
|
||
|
// 传播原始错误,让下一个拦截器(通用错误拦截器)处理。
|
||
|
return handler.next(error);
|
||
|
}
|
||
|
}
|
||
|
// 对于所有其他非 401 的错误,将错误传递给下一个拦截器。
|
||
|
return handler.next(error);
|
||
|
},
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/// 日志拦截器:在调试模式下打印详细的请求和响应日志。
|
||
|
Interceptor _getLoggerInterceptor() {
|
||
|
return PrettyDioLogger(
|
||
|
requestHeader: true,
|
||
|
requestBody: true,
|
||
|
responseHeader: true,
|
||
|
responseBody: true,
|
||
|
error: true,
|
||
|
compact: false,
|
||
|
maxWidth: 90,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/// 错误拦截器:处理通用的网络和服务器端错误,并显示 Snackbar 提示。
|
||
|
Interceptor _getErrorInterceptor() {
|
||
|
return InterceptorsWrapper(
|
||
|
onError: (error, handler) {
|
||
|
// 处理网络连接超时或未知网络错误。
|
||
|
if (error.type == DioExceptionType.connectionTimeout ||
|
||
|
error.type == DioExceptionType.receiveTimeout ||
|
||
|
error.type == DioExceptionType.sendTimeout) {
|
||
|
Get.snackbar('网络超时', '请检查网络连接后重试');
|
||
|
} else if (error.type == DioExceptionType.unknown) {
|
||
|
Get.snackbar('网络异常', '请检查网络连接后重试');
|
||
|
}
|
||
|
|
||
|
// 这部分代码只会在 AuthInterceptor 没有处理的错误(即非 401)时执行。
|
||
|
if (error.response != null) {
|
||
|
final message = _handleStatusCode(error.response!);
|
||
|
Get.snackbar('请求错误', message);
|
||
|
}
|
||
|
|
||
|
return handler.next(error);
|
||
|
},
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/// 辅助方法:根据 HTTP 状态码返回用户友好的错误信息。
|
||
|
String _handleStatusCode(Response response) {
|
||
|
switch (response.statusCode) {
|
||
|
case 400:
|
||
|
return response.data?['detail'] ?? '请求参数错误';
|
||
|
case 401:
|
||
|
return response.data?['detail'] ??
|
||
|
'未授权,请重新登录'; // 注意:对于 401 错误,这行代码理想情况下不会被执行。
|
||
|
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,
|
||
|
);
|
||
|
}
|
||
|
}
|