Files
AI-Health/health_app/lib/core/api_client.dart
MingNian 4d213b5a44 feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
2026-06-20 20:41:42 +08:00

128 lines
4.1 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 基础地址
const String baseUrl = 'http://localhost:5000'; // adb reverse 或同WiFi直连时改为PC的IP
/// 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 _dio.get(path, queryParameters: queryParameters);
}
/// 带 token 的 POST 请求
Future<Response> post(String path, {dynamic data}) async {
return _dio.post(path, data: data);
}
/// 带 token 的 PUT 请求
Future<Response> put(String path, {dynamic data}) async {
return _dio.put(path, data: data);
}
/// 带 token 的 DELETE 请求
Future<Response> delete(String path) async {
return _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 _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;
}
}
/// 认证拦截器:自动注入 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);
}
}