Files
jiu/client/lib/core/api/api_client.dart
T
wangjia cf646226a8 fix(client): 修复图片上传 content-type 被覆盖问题
BaseOptions 不应写死 Content-Type: application/json,
Dio 会根据 data 类型自动选择(FormData → multipart,Map → json)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 00:56:46 +08:00

132 lines
4.5 KiB
Dart
Raw 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 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../auth/auth_state.dart';
import '../config/app_config.dart';
import '../../providers/connectivity_provider.dart';
/// Public Dio instance for unauthenticated calls (login / refresh)
final _publicDio = Dio(BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 15),
));
final apiClientProvider = Provider<ApiClient>((ref) {
// 只监听登录/登出,不监听 token 内容变化。
// token refresh 只改 accessTokenisLoggedIn 不变,provider 不重建,
// 避免旧 Dio 被 close 后还在执行 fetch 导致 "adapter was closed"。
ref.watch(authStateProvider.select((s) => s.isLoggedIn));
final user = ref.read(authStateProvider).user;
final client = ApiClient(
token: user?.accessToken,
refreshToken: user?.refreshToken,
onTokenRefreshed: (newToken) {
ref.read(authStateProvider.notifier).updateAccessToken(newToken);
},
onAuthFailed: () {
if (ref.read(authStateProvider).isLoggedIn) {
ref.read(authStateProvider.notifier).logout();
}
},
onConnectionError: () {
ref.read(connectivityProvider.notifier).forceCheck();
},
);
ref.onDispose(client.dispose);
return client;
});
class ApiClient {
late final Dio _dio;
bool _disposed = false;
ApiClient({
String? token,
String? refreshToken,
void Function(String newToken)? onTokenRefreshed,
void Function()? onAuthFailed,
void Function()? onConnectionError,
}) {
_dio = Dio(BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 15),
headers: {
if (token != null) 'Authorization': 'Bearer $token',
},
));
// 网络错误 + 401 拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onError: (DioException e, ErrorInterceptorHandler handler) async {
if (_disposed) return handler.next(e);
// 网络层错误(连接拒绝、超时等)→ 立即触发连通性检测
final isNetworkError = e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.sendTimeout;
if (isNetworkError) {
onConnectionError?.call();
return handler.next(e);
}
if (e.response?.statusCode == 401 && (refreshToken ?? '').isNotEmpty) {
debugPrint('[ApiClient] got 401 on ${e.requestOptions.path}, trying refresh...');
try {
final resp = await _publicDio.post('/auth/refresh', data: {
'refresh_token': refreshToken,
});
final newToken = resp.data['data']['access_token'] as String;
_dio.options.headers['Authorization'] = 'Bearer $newToken';
if (!_disposed) onTokenRefreshed?.call(newToken);
final opts = e.requestOptions;
opts.headers['Authorization'] = 'Bearer $newToken';
final retryResp = await _dio.fetch(opts);
return handler.resolve(retryResp);
} catch (refreshErr) {
debugPrint('[ApiClient] refresh failed: $refreshErr');
if (!_disposed) onAuthFailed?.call();
}
} else if (e.response?.statusCode == 401) {
debugPrint('[ApiClient] got 401 on ${e.requestOptions.path}, no refresh token');
}
return handler.next(e);
},
),
);
}
/// 取消所有进行中的请求,标记实例为已废弃
void dispose() {
_disposed = true;
_dio.close(force: true);
}
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
_dio.get(path, queryParameters: params);
Future<List<int>> getBytes(String path) async {
final resp = await _dio.get<List<int>>(path,
options: Options(responseType: ResponseType.bytes));
return resp.data ?? [];
}
Future<Response> post(String path, {dynamic data}) =>
_dio.post(path, data: data);
Future<Response> put(String path, {dynamic data}) =>
_dio.put(path, data: data);
Future<Response> delete(String path) => _dio.delete(path);
}
/// Unauthenticated client for login/refresh
class PublicApiClient {
static Future<Response> post(String path, {dynamic data}) =>
_publicDio.post(path, data: data);
}