Files
ljwx-health/ljwx-phone/lib/services/api_client.dart
T

666 lines
20 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../config/app_config.dart';
import '../models/user_model.dart' as user_model;
import '../models/user_profile.dart';
import '../models/health_model.dart' as health_model;
import '../models/message_model.dart' as message_model;
import '../models/alert_model.dart' as alert_model;
import '../models/global_health_status.dart';
import '../models/health_metric.dart';
import '../models/risk_alert.dart';
import '../models/ai_health_suggestion.dart';
import 'auth_service_v2.dart';
import 'cache_service.dart';
import 'openapi_endpoints.dart';
import '../utils/error_handler.dart';
import '../utils/logger.dart';
/// 统一 API 客户端
/// 使用 Dio + 拦截器实现自动 token 管理 + 缓存策略
class ApiClient {
late final Dio _dio;
final CacheService _cache = CacheService();
// Store current user context for API calls (String to avoid precision issues)
String? _currentUserId;
String? _currentCustomerId;
List<String>? _currentOrgIds;
// 重试配置
static const int _maxRetries = 2;
static const int _retryDelayMs = 500;
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: AppConfig.instance.apiBaseUrl,
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
},
));
// 请求拦截器:自动添加 token + 性能监控
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await AuthServiceV2.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// 结构化日志记录
Logger.i(
'API Request: ${options.method} ${options.path}',
tag: 'API',
data: {
'method': options.method,
'path': options.path,
'query': options.queryParameters,
'hasBody': options.data != null,
},
);
// 开始性能追踪(存储在 extra 中)
options.extra['_api_tracker'] = ApiPerformanceMonitor.startTracking(
options.path,
options.method,
);
return handler.next(options);
},
onResponse: (response, handler) {
// 结束性能追踪
final tracker = response.requestOptions.extra['_api_tracker'] as ApiCallTracker?;
tracker?.finish(
success: true,
statusCode: response.statusCode,
);
Logger.i(
'API Response: ${response.statusCode}',
tag: 'API',
data: {
'status': response.statusCode,
'path': response.requestOptions.path,
'hasData': response.data != null,
},
);
return handler.next(response);
},
onError: (error, handler) async {
// 结束性能追踪(失败)
final tracker = error.requestOptions.extra['_api_tracker'] as ApiCallTracker?;
tracker?.finish(
success: false,
statusCode: error.response?.statusCode,
errorMessage: error.message,
);
// 使用 ErrorHandler 处理错误
final errorMessage = ErrorHandler.handleError(
error,
context: '${error.requestOptions.method} ${error.requestOptions.path}',
);
Logger.e(
'API Error: $errorMessage',
tag: 'API',
error: error,
data: {
'status': error.response?.statusCode,
'path': error.requestOptions.path,
'type': error.type.toString(),
},
);
// Token 过期自动刷新
if (error.response?.statusCode == 401) {
Logger.w('Token expired, attempting refresh', tag: 'AUTH');
final refreshed = await AuthServiceV2.refreshToken();
if (refreshed) {
// 重试原请求
try {
final options = error.requestOptions;
final token = await AuthServiceV2.getAccessToken();
options.headers['Authorization'] = 'Bearer $token';
// 创建新的追踪器(重试请求)
options.extra['_api_tracker'] = ApiPerformanceMonitor.startTracking(
options.path,
options.method,
);
final response = await _dio.fetch(options);
Logger.i('Token refresh successful, retry succeeded', tag: 'AUTH');
return handler.resolve(response);
} catch (e) {
Logger.e('Token refresh retry failed', tag: 'AUTH', error: e);
}
}
}
return handler.next(error);
},
));
}
/// 网络请求重试包装器(仅重试网络错误)
Future<T?> _retryOnNetworkError<T>(
Future<T> Function() operation, {
String? cacheKey,
}) async {
int retries = 0;
while (retries <= _maxRetries) {
try {
return await operation();
} on DioException catch (e) {
final isNetworkError = ErrorHandler.isNetworkError(e);
if (isNetworkError && retries < _maxRetries) {
retries++;
final delayMs = _retryDelayMs * retries; // 指数退避
Logger.w(
'Network error, retrying attempt $retries/$_maxRetries after ${delayMs}ms',
tag: 'RETRY',
data: {'error': e.type.toString(), 'delay': delayMs},
);
await Future.delayed(Duration(milliseconds: delayMs));
continue;
}
// 非网络错误或已达最大重试次数,返回缓存数据(即使过期)
if (cacheKey != null) {
final expired = _cache.get<T>(cacheKey);
if (expired != null) {
Logger.w(
'Using stale cache as fallback',
tag: 'CACHE',
data: {'cacheKey': cacheKey},
);
return expired;
}
}
final errorMessage = ErrorHandler.handleError(e, context: 'Retry wrapper');
Logger.e(
'Request failed after retries: $errorMessage',
tag: 'RETRY',
error: e,
);
return null;
} catch (e) {
Logger.e('Unexpected error in retry wrapper', tag: 'RETRY', error: e);
return null;
}
}
return null;
}
/// 获取当前用户个人资料(带缓存)
Future<UserProfile?> getUserProfile({bool forceRefresh = false}) async {
const cacheKey = 'user_profile';
if (!forceRefresh) {
final cached = _cache.get<UserProfile>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
try {
final response = await _dio.get(OpenApiEndpoints.mobileUserInfo);
if (response.data['code'] == 200) {
final data = response.data['data'];
final profile = UserProfile.fromJson(data);
_cache.set(cacheKey, profile, ttl: Duration(minutes: 10));
return profile;
}
debugPrint('Get user profile failed: ${response.data['message']}');
return null;
} catch (e) {
debugPrint('Get user profile error: $e');
return null;
}
}
/// 获取用户信息(带缓存)
Future<user_model.UserInfo?> getUserInfo({bool forceRefresh = false}) async {
const cacheKey = 'user_info';
if (!forceRefresh) {
final cached = _cache.get<user_model.UserInfo>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
try {
final response = await _dio.get(OpenApiEndpoints.mobileUserInfo);
if (response.data['code'] == 200) {
final data = response.data['data'];
// Extract user context for API calls (String type)
_currentUserId = data['userId']?.toString();
_currentCustomerId = data['customerId']?.toString();
if (data['orgIds'] != null) {
_currentOrgIds = (data['orgIds'] as List).map((e) => e.toString()).toList();
}
debugPrint('[UserContext] userId=$_currentUserId, customerId=$_currentCustomerId, orgIds=$_currentOrgIds');
final userInfo = user_model.UserInfo.fromJson(data);
_cache.set(cacheKey, userInfo, ttl: Duration(minutes: 10));
return userInfo;
}
debugPrint('Get user info failed: ${response.data['message']}');
return null;
} catch (e) {
debugPrint('Get user info error: $e');
return null;
}
}
/// 获取健康数据
Future<List<health_model.HealthDataRecord>> getHealthData({
required String startDate,
required String endDate,
int page = 1,
int pageSize = 20,
}) async {
try {
final queryParams = {
'startDate': startDate,
'endDate': endDate,
'page': page,
'pageSize': pageSize,
};
// User context (userId, customerId, orgId) is automatically injected by backend
// No need to pass these parameters manually
final response = await _dio.get(OpenApiEndpoints.mobileHealthData, queryParameters: queryParams);
if (response.data['code'] == 200) {
final records = response.data['data']['records'] as List;
return records
.map((e) => health_model.HealthDataRecord.fromJson(e))
.toList();
}
debugPrint('Get health data failed: ${response.data['message']}');
return [];
} catch (e) {
debugPrint('Get health data error: $e');
return [];
}
}
/// 获取今日健康数据(带缓存)
Future<health_model.HealthData?> getTodayHealthData({bool forceRefresh = false}) async {
final today = DateTime.now();
final dateStr = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
final cacheKey = 'today_health_data_$dateStr';
if (!forceRefresh) {
final cached = _cache.get<health_model.HealthData>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
final dataList = await getHealthData(
startDate: dateStr,
endDate: dateStr,
page: 1,
pageSize: 1,
);
if (dataList.isNotEmpty) {
// Wrap HealthDataRecord in HealthData object
final record = dataList.first;
final healthData = health_model.HealthData(
departmentStats: {},
deviceCount: 1,
healthData: [record],
orgId: '',
statistics: health_model.HealthStatistics(
averageStats: health_model.AverageStats(
avgBloodOxygen: record.bloodOxygen ?? 0,
avgCalorie: record.calorie ?? 0,
avgDistance: record.distance ?? 0,
avgHeartRate: record.heartRate ?? 0,
avgStep: record.step?.toDouble() ?? 0,
avgTemperature: record.temperature ?? 0,
),
devicesWithData: 1,
totalDevices: 1,
),
totalRecords: 1,
userId: '',
vitals: {
'heartRate': record.heartRate,
'bloodOxygen': record.bloodOxygen,
'temperature': record.temperature,
'pressureHigh': record.pressureHigh,
'pressureLow': record.pressureLow,
'step': record.step,
'distance': record.distance,
'calorie': record.calorie,
'stress': record.stress,
},
);
_cache.set(cacheKey, healthData, ttl: Duration(minutes: 3));
return healthData;
}
return null;
}
/// 获取消息列表
Future<message_model.MessageListResponse> getMessages({
int page = 1,
int pageSize = 20,
String? status,
}) async {
try {
// Use new mobile API endpoint
final response = await _dio.get(OpenApiEndpoints.mobileMessages, queryParameters: {
'page': page,
'pageSize': pageSize,
if (status != null) 'status': status,
});
if (response.data['code'] == 200 || response.statusCode == 200) {
// Handle both response formats
if (response.data['data'] is Map && response.data['data']['records'] != null) {
return message_model.MessageListResponse.fromJson(response.data['data']);
}
// Fallback to empty response
return message_model.MessageListResponse.empty();
}
debugPrint('Get messages failed: ${response.data['message']}');
return message_model.MessageListResponse.empty();
} catch (e) {
debugPrint('Get messages error: $e');
return message_model.MessageListResponse.empty();
}
}
/// 获取未读消息数量
Future<int> getUnreadMessageCount() async {
try {
// Return 0 for now as endpoint may not exist
return 0;
} catch (e) {
debugPrint('Get unread message count error: $e');
return 0;
}
}
/// 标记消息已读
Future<bool> markMessageAsRead(String messageId) async {
try {
// Not implemented yet
return false;
} catch (e) {
debugPrint('Mark message as read error: $e');
return false;
}
}
/// 获取告警列表
Future<alert_model.AlertListResponse> getAlerts({
int page = 1,
int pageSize = 20,
String? status,
String? level,
}) async {
try {
final response = await _dio.get(OpenApiEndpoints.mobileAlerts, queryParameters: {
'page': page,
'pageSize': pageSize,
if (status != null) 'status': status,
if (level != null) 'level': level,
});
if (response.data['code'] == 200) {
debugPrint('✅ [API] Get alerts response: ${response.data['data']}');
final alertListResponse = alert_model.AlertListResponse.fromJson(response.data['data']);
debugPrint('✅ [API] Parsed ${alertListResponse.records.length} alerts');
if (alertListResponse.records.isNotEmpty) {
final firstAlert = alertListResponse.records.first;
debugPrint('✅ [API] First alert - alertDesc: "${firstAlert.alertDesc}", alertType: "${firstAlert.alertType}", alertTimestamp: "${firstAlert.alertTimestamp}"');
}
return alertListResponse;
}
debugPrint('Get alerts failed: ${response.data['message']}');
return alert_model.AlertListResponse.empty();
} catch (e) {
debugPrint('Get alerts error: $e');
return alert_model.AlertListResponse.empty();
}
}
/// 获取未处理告警数量
Future<int> getUnprocessedAlertCount() async {
try {
final response = await _dio.get(OpenApiEndpoints.mobileAlertsUnprocessedCount);
if (response.data['code'] == 200) {
return (response.data['data'] as num?)?.toInt() ?? 0;
}
return 0;
} catch (e) {
debugPrint('Get unprocessed alert count error: $e');
return 0;
}
}
/// 获取全局健康状态(带重试和缓存降级)
Future<GlobalHealthStatus?> getGlobalHealthStatus({bool forceRefresh = false}) async {
const cacheKey = 'global_health_status';
if (!forceRefresh) {
final cached = _cache.get<GlobalHealthStatus>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
return await _retryOnNetworkError<GlobalHealthStatus?>(
() async {
final response = await _dio.get(OpenApiEndpoints.mobileHealthStatusGlobal);
if (response.data['code'] == 200) {
final status = GlobalHealthStatus.fromJson(response.data['data']);
_cache.set(cacheKey, status, ttl: Duration(minutes: 2));
return status;
}
debugPrint('Get global health status failed: ${response.data['message']}');
return null;
},
cacheKey: cacheKey,
);
}
/// 获取关键健康指标(带重试和缓存降级)
Future<List<HealthMetric>> getKeyHealthMetrics({bool forceRefresh = false}) async {
const cacheKey = 'key_health_metrics';
if (!forceRefresh) {
final cached = _cache.get<List<HealthMetric>>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
final result = await _retryOnNetworkError<List<HealthMetric>>(
() async {
final response = await _dio.get(OpenApiEndpoints.mobileHealthMetricsKey);
if (response.data['code'] == 200) {
final List<dynamic> data = response.data['data'];
final metrics = data.map((e) => HealthMetric.fromJson(e)).toList();
_cache.set(cacheKey, metrics, ttl: Duration(minutes: 2));
return metrics;
}
debugPrint('Get key health metrics failed: ${response.data['message']}');
return [];
},
cacheKey: cacheKey,
);
return result ?? [];
}
/// 获取风险告警摘要(带重试和缓存降级)
Future<RiskAlert?> getRiskAlertSummary({int hours = 24, bool forceRefresh = false}) async {
const cacheKey = 'risk_alert_summary';
if (!forceRefresh) {
final cached = _cache.get<RiskAlert>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
return await _retryOnNetworkError<RiskAlert?>(
() async {
final response = await _dio.get(OpenApiEndpoints.mobileHealthAlertsSummary, queryParameters: {
'hours': hours,
});
if (response.data['code'] == 200) {
final data = response.data['data'];
if (data != null) {
final alert = RiskAlert.fromJson(data);
_cache.set(cacheKey, alert, ttl: Duration(minutes: 5));
return alert;
}
}
debugPrint('Get risk alert summary failed: ${response.data['message']}');
return null;
},
cacheKey: cacheKey,
);
}
/// 获取 AI 健康建议(带重试和缓存降级)
Future<AIHealthSuggestion?> getAIHealthSuggestions({bool forceRefresh = false}) async {
const cacheKey = 'ai_health_suggestions';
if (!forceRefresh) {
final cached = _cache.get<AIHealthSuggestion>(cacheKey);
if (cached != null) {
debugPrint('[Cache] Hit: $cacheKey');
return cached;
}
}
return await _retryOnNetworkError<AIHealthSuggestion?>(
() async {
final response = await _dio.get(OpenApiEndpoints.mobileHealthAiSuggestions);
if (response.data['code'] == 200) {
final data = response.data['data'];
if (data != null) {
final suggestion = AIHealthSuggestion.fromJson(data);
_cache.set(cacheKey, suggestion, ttl: Duration(minutes: 10));
return suggestion;
}
}
debugPrint('Get AI health suggestions failed: ${response.data['message']}');
return null;
},
cacheKey: cacheKey,
);
}
/// 处理告警
Future<bool> processAlert(String alertId, String status) async {
try {
final response = await _dio.post(OpenApiEndpoints.mobileAlertsProcess, data: {
'alertId': alertId,
'status': status,
'processedTime': DateTime.now().toIso8601String(),
});
return response.data['code'] == 200;
} catch (e) {
debugPrint('Process alert error: $e');
return false;
}
}
/// 上传健康数据(保留兼容旧接口)
Future<bool> uploadHealthData(Map<String, dynamic> healthData) async {
try {
final response = await _dio.post(OpenApiEndpoints.uploadHealthData, data: healthData);
return response.statusCode == 200;
} catch (e) {
debugPrint('Upload health data error: $e');
return false;
}
}
/// 上传设备信息(保留兼容旧接口)
Future<bool> uploadDeviceInfo(Map<String, dynamic> deviceInfo) async {
try {
final response = await _dio.post(OpenApiEndpoints.uploadDeviceInfo, data: deviceInfo);
return response.statusCode == 200;
} catch (e) {
debugPrint('Upload device info error: $e');
return false;
}
}
/// 重置密码
Future<bool> resetPassword(String oldPassword, String newPassword) async {
try {
// Use mobile API endpoint
final response = await _dio.post(OpenApiEndpoints.mobileResetPassword, data: {
'oldPassword': oldPassword,
'newPassword': newPassword,
});
return response.data['code'] == 200 || response.statusCode == 200;
} catch (e) {
debugPrint('Reset password error: $e');
return false;
}
}
/// 更新用户信息
Future<bool> updateUserInfo(Map<String, dynamic> userInfo) async {
try {
final response = await _dio.put(OpenApiEndpoints.authUserInfo, data: userInfo);
return response.data['code'] == 200;
} catch (e) {
debugPrint('Update user info error: $e');
return false;
}
}
}