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) { final authState = ref.watch(authStateProvider); final myToken = authState.user?.accessToken; final client = ApiClient( token: myToken, refreshToken: authState.user?.refreshToken, onTokenRefreshed: (newToken) { debugPrint('[ApiClient] token refreshed'); if (ref.read(authStateProvider).user?.accessToken == myToken) { ref.read(authStateProvider.notifier).updateAccessToken(newToken); } }, onAuthFailed: () { debugPrint('[ApiClient] onAuthFailed! myToken prefix: ${(myToken ?? '').substring(0, (myToken ?? '').length.clamp(0, 20))}'); if (ref.read(authStateProvider).user?.accessToken == myToken) { debugPrint('[ApiClient] calling logout()'); ref.read(authStateProvider.notifier).logout(); } else { debugPrint('[ApiClient] skipping logout (stale client)'); } }, ); 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; if (!_disposed) onTokenRefreshed?.call(newToken); // 用新 token 重试原请求 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); }