// api_client.dart — 统一鉴权 HTTP 客户端。 // // 职责:为受 JWT 保护的端点(/v1/me、/v1/me/devices、/v1/plans、/v1/usage、 // /v1/redeem、/v1/ads/unlock 等)提供 GET/POST/DELETE 封装,自动: // - 注入 Authorization: Bearer // - 401 时调用 [refresh] 刷新令牌,成功则用新令牌重试一次 // - 错误统一包装为 AuthApiException(message_zh/en),UI 复用既有错误模型 import 'dart:convert'; import 'package:http/http.dart' as http; import 'auth_api.dart' show AuthApiException; /// 取当前 access token(可空表示未登录)。 typedef TokenGetter = String? Function(); /// 触发刷新;成功返回 true(调用方应已写入新令牌)。 typedef TokenRefresher = Future Function(); class ApiClient { ApiClient({ required this.baseUrl, required this.getToken, required this.refresh, http.Client? client, }) : _client = client ?? http.Client(); final String baseUrl; final TokenGetter getToken; final TokenRefresher refresh; final http.Client _client; /// GET → 解析为 JSON 对象。 Future> getJson(String path) async => _decodeObj(await _send(() => _req('GET', path))); /// POST → 解析为 JSON 对象(空响应体返回空 map)。 Future> postJson(String path, [Map? body]) async => _decodeObj(await _send(() => _req('POST', path, body))); /// DELETE(无响应体)。 Future delete(String path) async { await _send(() => _req('DELETE', path)); } // ── 内部 ────────────────────────────────────────────────────────── /// 发送 + 401 自动刷新重试一次;非 2xx 抛 AuthApiException。 Future _send(Future Function() build) async { http.Response resp; try { resp = await build(); if (resp.statusCode == 401 && await refresh()) { resp = await build(); } } on AuthApiException { rethrow; } on Exception catch (e) { throw AuthApiException( statusCode: -1, messageZh: '网络请求失败,请检查连接后重试', messageEn: 'Network error: $e', ); } if (resp.statusCode < 200 || resp.statusCode >= 300) _throwFromResponse(resp); return resp; } Future _req(String method, String path, [Map? body]) { final uri = Uri.parse('$baseUrl$path'); final headers = {'Content-Type': 'application/json'}; final t = getToken(); if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t'; final Future f; switch (method) { case 'GET': f = _client.get(uri, headers: headers); case 'DELETE': f = _client.delete(uri, headers: headers); default: f = _client.post(uri, headers: headers, body: body == null ? null : jsonEncode(body)); } return f.timeout(const Duration(seconds: 15)); } Map _decodeObj(http.Response r) { if (r.body.isEmpty) return {}; return jsonDecode(r.body) as Map; } Never _throwFromResponse(http.Response resp) { String zh = '操作失败 (HTTP ${resp.statusCode})'; String en = 'Request failed (HTTP ${resp.statusCode})'; String? code; try { final b = jsonDecode(resp.body) as Map; zh = b['message_zh'] as String? ?? zh; en = b['message_en'] as String? ?? en; code = b['code'] as String?; } catch (_) {} throw AuthApiException(statusCode: resp.statusCode, messageZh: zh, messageEn: en, code: code); } void dispose() => _client.close(); }