Files
AI-Health/health_app/lib/core/api_client.dart
MingNian 4507083f3f refactor: 重构蓝牙设备页与新增设备页的扫描-连接-录入流程
- 通用 BLE 健康设备识别(血压计/血糖仪/体重秤/血氧仪 标准 service UUID)
- 蓝牙设备页静默自动同步:仅匹配已绑定的设备名+类型,收到数据后变绿
- 新增设备页:扫描所有支持类型设备 → 选连接 → 绑定 + 同步首次数据
- 修复 read 缓存导致的重复录入循环(删除主动 read,依赖 indicate 推送)
- 修复 isConnected 缓存状态导致连接异常(强制先 disconnect 再 connect)
- 录入弹窗:血压心率同等级展示、3 秒自动关闭/手动确认
- 顺手更新本机开发 IP 与清理过时设计文档
2026-06-28 22:53:35 +08:00

181 lines
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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://10.4.232.251: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<String?> get accessToken => _db.read('access_token');
Future<String?> get refreshToken => _db.read('refresh_token');
Future<void> saveTokens(String access, String refresh) async {
await _db.write('access_token', access);
await _db.write('refresh_token', refresh);
}
Future<void> clearTokens() async {
await _db.delete('access_token');
await _db.delete('refresh_token');
}
/// 带 token 的 GET 请求
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
return _request(_dio.get(path, queryParameters: queryParameters));
}
/// 带 token 的 POST 请求
Future<Response> post(String path, {dynamic data}) async {
return _request(_dio.post(path, data: data));
}
/// 带 token 的 PUT 请求
Future<Response> put(String path, {dynamic data}) async {
return _request(_dio.put(path, data: data));
}
/// 带 token 的 DELETE 请求
Future<Response> delete(String path) async {
return _request(_dio.delete(path));
}
/// 上传文件multipart返回文件 URL
Future<String?> 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<Response> _request(Future<Response> 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);
}
}