import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../auth/auth_state.dart'; const _baseUrl = 'http://localhost:8080/api/v1'; /// Public Dio instance for unauthenticated calls (login / refresh) final _publicDio = Dio(BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), headers: {'Content-Type': 'application/json'}, )); final apiClientProvider = Provider((ref) { // 只监听登录/登出,不监听 token 内容变化。 // token refresh 只改 accessToken,isLoggedIn 不变,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(); } }, ); 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, }) { _dio = Dio(BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }, )); // 401 auto-refresh 拦截器 if (refreshToken != null) { _dio.interceptors.add( InterceptorsWrapper( onError: (DioException e, ErrorInterceptorHandler handler) async { // 如果这个 client 已被替换,忽略所有回调 if (_disposed) { 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 默认 headers,后续所有请求生效 _dio.options.headers['Authorization'] = 'Bearer $newToken'; if (!_disposed) onTokenRefreshed?.call(newToken); // 用新 token 重试原请求(此时 _dio 仍存活,不会 adapter closed) 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 get(String path, {Map? params}) => _dio.get(path, queryParameters: params); Future post(String path, {dynamic data}) => _dio.post(path, data: data); Future put(String path, {dynamic data}) => _dio.put(path, data: data); Future delete(String path) => _dio.delete(path); } /// Unauthenticated client for login/refresh class PublicApiClient { static Future post(String path, {dynamic data}) => _publicDio.post(path, data: data); }