import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; import 'local_database.dart'; /// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。 const String baseUrl = String.fromEnvironment( 'API_BASE_URL', defaultValue: 'http://localhost:5000', ); class ApiException implements Exception { final int? code; final String message; const ApiException(this.message, {this.code}); @override String toString() => message; } /// Dio HTTP 客户端封装——带 token 注入、401 自动刷新 class ApiClient { final Dio _dio; final LocalDatabase _db; ApiClient({required LocalDatabase db}) : _db = db, _dio = Dio(BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 60), headers: {'Content-Type': 'application/json'}, )) { _dio.interceptors.add(_AuthInterceptor(this)); _dio.interceptors.add(LogInterceptor(requestBody: false, responseBody: false)); } Dio get dio => _dio; Future get accessToken => _db.read('access_token'); Future get refreshToken => _db.read('refresh_token'); Future saveTokens(String access, String refresh) async { await _db.write('access_token', access); await _db.write('refresh_token', refresh); } Future clearTokens() async { await _db.delete('access_token'); await _db.delete('refresh_token'); } /// 带 token 的 GET 请求 Future get(String path, {Map? queryParameters}) async { return _request(_dio.get(path, queryParameters: queryParameters)); } /// 带 token 的 POST 请求 Future post(String path, {dynamic data}) async { return _request(_dio.post(path, data: data)); } /// 带 token 的 PUT 请求 Future put(String path, {dynamic data}) async { return _request(_dio.put(path, data: data)); } /// 带 token 的 DELETE 请求 Future delete(String path) async { return _request(_dio.delete(path)); } /// 上传文件(multipart),返回文件 URL Future uploadFile(String path, File file, {String fieldName = 'file'}) async { final formData = FormData.fromMap({ fieldName: await MultipartFile.fromFile(file.path, filename: file.path.split('/').last), }); final res = await _request(_dio.post(path, data: formData)); final data = res.data; if (data is Map) { final topUrl = data['url']?.toString(); if (topUrl != null && topUrl.isNotEmpty) return topUrl; final payload = data['data']; if (payload is Map) { final url = payload['url']?.toString(); return url != null && url.isNotEmpty ? url : null; } if (payload is List && payload.isNotEmpty && payload.first is Map) { final first = payload.first as Map; final url = first['url']?.toString(); return url != null && url.isNotEmpty ? url : null; } } return null; } Response _ensureBusinessSuccess(Response response) { final body = response.data; if (body is Map && body.containsKey('code')) { final rawCode = body['code']; final code = rawCode is int ? rawCode : int.tryParse('$rawCode'); if (code != null && code != 0) { throw ApiException( body['message']?.toString().trim().isNotEmpty == true ? body['message'].toString() : '请求失败', code: code, ); } } return response; } Future _request(Future request) async { try { return _ensureBusinessSuccess(await request); } on DioException catch (error) { final body = error.response?.data; final message = body is Map && body['message']?.toString().trim().isNotEmpty == true ? body['message'].toString() : error.type == DioExceptionType.connectionTimeout || error.type == DioExceptionType.receiveTimeout ? '请求超时,请稍后重试' : error.type == DioExceptionType.connectionError ? '无法连接服务器,请检查网络或后端地址' : '请求失败,请稍后重试'; final rawCode = body is Map ? body['code'] : null; throw ApiException( message, code: rawCode is int ? rawCode : int.tryParse('$rawCode') ?? error.response?.statusCode, ); } } } /// 认证拦截器:自动注入 token + 401 刷新 class _AuthInterceptor extends Interceptor { final ApiClient _client; _AuthInterceptor(this._client); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { if (!options.path.contains('/auth/')) { final token = await _client.accessToken; if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } } handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401) { final refresh = await _client.refreshToken; if (refresh != null) { try { final response = await Dio(BaseOptions(baseUrl: baseUrl)) .post('/api/auth/refresh', data: {'refreshToken': refresh}); final data = response.data['data']; if (data != null) { await _client.saveTokens(data['accessToken'], data['refreshToken']); final opts = err.requestOptions; final token = data['accessToken']; opts.headers['Authorization'] = 'Bearer $token'; final retryResponse = await Dio(BaseOptions(baseUrl: baseUrl)).fetch(opts); return handler.resolve(retryResponse); } } catch (e) { log('[ApiClient] token刷新失败: $e'); } } await _client.clearTokens(); } handler.next(err); } }