import 'package:dio/dio.dart'; import 'package:patbond_flutter/core/network/api_envelope.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/network/token_refresher.dart'; import 'package:patbond_flutter/features/auth/session_manager.dart'; /// API 基地址:`--dart-define=PATBOND_API_BASE_URL=...` 注入,默认本机后端。 const String patbondApiBaseUrl = String.fromEnvironment( 'PATBOND_API_BASE_URL', defaultValue: 'http://127.0.0.1:8081', ); /// 构建全局共用的 Dio 实例。 /// /// `validateStatus` 放行所有状态码:错误信封由 [ApiClient] 统一解析成 /// 类型化异常,不依赖 dio 的 4xx/5xx 抛错路径。 Dio buildPatbondDio({required SessionManager session, String? baseUrl}) { final dio = Dio( BaseOptions( baseUrl: baseUrl ?? patbondApiBaseUrl, connectTimeout: const Duration(seconds: 5), sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), contentType: Headers.jsonContentType, validateStatus: (_) => true, ), ); dio.interceptors.add(AuthInterceptor(session)); return dio; } /// 鉴权拦截器:对标记 [ApiClient.requiresAuthExtra] 的请求附加 /// `Authorization: Bearer`,并统一携带设备标识。 class AuthInterceptor extends Interceptor { AuthInterceptor(this._session); final SessionManager _session; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final deviceId = _session.deviceId; if (deviceId != null) { options.headers['X-Device-Id'] = deviceId; } if (options.extra[ApiClient.requiresAuthExtra] == true) { final token = _session.accessToken; if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } } handler.next(options); } } /// HTTP 客户端封装:错误信封 → 类型化异常,401/40101 → 刷新后重放一次。 class ApiClient { ApiClient({ required this._dio, required this._session, required this._refresher, }); /// RequestOptions.extra 标记:该请求需要 Bearer 鉴权。 static const requiresAuthExtra = 'patbond.requiresAuth'; final Dio _dio; final SessionManager _session; final TokenRefresher _refresher; /// 发起请求并返回信封中的 `data`。 /// /// 鉴权请求遇 HTTP 401 或 code 40101 时:经单飞刷新后原样重放一次; /// 重放仍失败则清会话并抛 [SessionExpiredException]。 Future request( String path, { String method = 'POST', Object? body, Map? headers, bool requiresAuth = false, }) async { var response = await _send( path, method: method, body: body, headers: headers, requiresAuth: requiresAuth, ); if (requiresAuth && _isAuthFailure(response)) { await _refresher.refresh(); response = await _send( path, method: method, body: body, headers: headers, requiresAuth: requiresAuth, ); if (_isAuthFailure(response)) { await _session.clearSession(); throw const SessionExpiredException('刷新后重放仍未通过鉴权'); } } return _unwrap(response); } Future> _send( String path, { required String method, required bool requiresAuth, Object? body, Map? headers, }) async { try { return await _dio.request( path, data: body, options: Options( method: method, headers: headers, extra: {requiresAuthExtra: requiresAuth}, ), ); } on DioException catch (error) { throw ApiNetworkException('请求失败:${error.type.name}'); } } bool _isAuthFailure(Response response) { if (response.statusCode == 401) return true; return ApiEnvelope.tryParse(response.data)?.code == ApiCodes.accessTokenInvalid; } Object? _unwrap(Response response) { final status = response.statusCode ?? 0; if (status == 429) { throw const ApiRateLimitException(); } final envelope = ApiEnvelope.tryParse(response.data); if (envelope == null || status >= 500) { throw ApiNetworkException('响应异常:HTTP $status'); } if (envelope.code == ApiCodes.ok) { return envelope.data; } throw ApiBusinessException(code: envelope.code, message: envelope.message); } }