Files
customer-im-client-dev/apps/im_app/lib/app/di/network_provider.dart
2026-03-09 19:05:55 +08:00

572 lines
28 KiB
Dart
Raw Permalink 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:async';
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
import 'package:im_app/core/foundation/config.dart';
import 'package:im_app/core/foundation/constants.dart';
import 'package:im_app/core/foundation/device_info.dart';
import 'package:im_app/core/foundation/errors.dart';
import 'package:im_app/core/foundation/utils.dart';
import 'package:im_app/core/services/network_monitor.dart';
import 'package:im_app/core/services/socket_manager.dart';
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
/// 网络状态监听 Provider全局单例
///
/// 基于 connectivity_plus 监听平台网络变化,
/// 作为公共服务供多个模块使用:
/// - SocketManager网络变化时自动断连/重连 WebSocket
/// - HTTP 层:请求前检查网络可用性
/// - UI 层:显示网络状态提示
///
/// ## 使用
///
/// ```dart
/// // 查询当前状态
/// final isConnected = ref.read(networkMonitorProvider).isConnected;
///
/// // 监听状态变化
/// ref.listen(networkMonitorProvider, (prev, monitor) {
/// monitor.onStatusChanged.listen((isAvailable) {
/// // 处理网络变化
/// });
/// });
/// ```
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
final monitor = NetworkMonitor(onLog: _makeLogger('Network'));
ref.onDispose(() {
monitor.dispose();
});
return monitor;
});
// ── Token 更新事件流 ─────────────────────────────────────────────────────────
/// Token 更新事件流
///
/// apiConfigProvider.onTokenUpdated → 推送新 token 到此流
/// socketManagerProvider → 监听此流 → 同步 token 到 WebSocket
/// onBeforeReconnect 中刷新 token 后调用 apiConfig.updateToken → tokenStream.add
/// 需要同步传播到 socketManager.updateToken → socketClient._currentToken
/// 确保随后的 _doConnect() 使用新 token。异步模式下 _doConnect 会在 stream
final _tokenUpdateStreamProvider = Provider<StreamController<String>>((ref) {
final controller = StreamController<String>.broadcast(sync: true);
ref.onDispose(controller.close);
return controller;
});
// ── HTTP 基础设施 ─────────────────────────────────────────────────────────────
/// API 配置 Provider全局单例
///
/// 从 [AppConfig.apiBaseUrl]config.json → --dart-define-from-file读取 baseURL
/// 注入到 Network SDK 作为所有 HTTP 请求的基础 URL。
///
/// [onCheckNetworkAvailable] 由 [networkMonitorProvider](公共服务)注入,
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
final apiConfigProvider = Provider<ApiConfig>((ref) {
final networkMonitor = ref.read(networkMonitorProvider);
final tokenStream = ref.read(_tokenUpdateStreamProvider);
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {
'platform': DeviceInfo.platform,
'os-type': DeviceInfo.osType.toString(),
'client-version': AppConfig.appVersion,
'channel': AppConfig.channel,
'lang': DeviceInfo.lang,
'device-id': DeviceInfo.deviceId,
'device-name': DeviceInfo.deviceName,
},
onTokenRefresh: () async {
// TODO: App 层刷新 token 逻辑
return null;
},
onTokenUpdated: (newToken) {
// 通过事件流同步到 WebSocket避免直接引用 socketManagerProvider 造成循环依赖
tokenStream.add(newToken);
},
onCheckNetworkAvailable: _checkNetwork(networkMonitor),
// TODO: 接入 cipher_guard_sdk 后注入请求加密回调。
// 前提AuthNotifier.login() 中已完成 cipherSdk.setActiveKeyPair(pub, priv)。
// 示例:
// onEncryptRequest: (path, headers, body) async {
// final encryptedKey = await cipherSdk.encryptSessionKeyWithActiveKey(
// sessionKey: currentSessionKey,
// );
// return EncryptedRequest(body: encryptedBody, headers: {'X-Key': encryptedKey});
// },
onEncryptRequest: null,
// TODO: 接入 cipher_guard_sdk 后注入响应解密回调。
// 前提:与 onEncryptRequest 配套,服务端响应同样加密时启用。
// 示例:
// onDecryptResponse: (data) async {
// final plaintext = await cipherSdk.decryptMessage(encryptedData: data as String, ...);
// return jsonDecode(plaintext) as Map<String, dynamic>;
// },
onDecryptResponse: null,
onBusinessError: (code, message, path) {
switch (code) {
// Token 过期SDK 自动刷 token + 重试,业务层无感
case ApiErrorCodes.tokenInvalid:
case ApiErrorCodes.jwtInvalid:
case ApiErrorCodes.sessionInvalid:
return BusinessErrorAction.refreshToken;
// Token 刷新失败 / refresh token 失效:强制登出
case ApiErrorCodes.refreshTokenFailed:
// TODO: 清除登录态,跳转登录页
return BusinessErrorAction.forceLogout;
// 踢下线:账号在其他设备登录、签名/密钥异常
case ApiErrorCodes.loggedInAnotherDevice:
case ApiErrorCodes.signingMethodError:
case ApiErrorCodes.parsingKeyError:
// TODO: 接入全局 Toast/弹窗机制后展示踢下线提示,并跳转登录页
return BusinessErrorAction.handled;
// 触发图片验证:需展示 CAPTCHA 后重发 OTP
// data 中含 android / ios / web 平台 token见 SendOtpCaptchaData
case ApiErrorCodes.captchaRequired:
// TODO: 接入 CAPTCHA SDK验证通过后重发 OTP
return BusinessErrorAction.handled;
default:
// 单接口自行处理ViewModel 的 guard 会收到 ApiError
return BusinessErrorAction.unhandled;
}
},
onTransformResponse:
null, // TODO: 如后端响应格式非标准,在此归一化为 { code, data, message }
onGetTokenExpiry: parseJwtExpiry,
maxRetries: AppConstants.maxRetries,
retryBaseDelay: AppConstants.retryBaseDelay,
onLog: _makeLogger('Network'),
);
});
/// API 客户端 Provider全局单例
///
/// 含拦截器Auth / Retry / Logging、超时配置。
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
final config = ref.read(apiConfigProvider);
return NetworksSdkApi()..initialize(config);
});
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
/// SocketConfig Provider内部使用不对外暴露
///
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
/// SDK 内部不调用其他 SDK。
final _socketConfigProvider = Provider<SocketConfig>((ref) {
final networkMonitor = ref.read(networkMonitorProvider);
final apiConfig = ref.read(apiConfigProvider);
return SocketConfig(
maxReconnectAttempts: AppConstants.maxRetries,
maxReconnectDelay: AppConstants.maxReconnectDelay,
unlimitedReconnect: true, // IM 场景始终保持连接
// 接入 cipher_guard_sdk 后改为 cipher=true&type=mode3
onBuildConnectUrl: (url, token) {
final uri = Uri.parse(url);
final params = <String, String>{
...uri.queryParameters,
if (token != null) 'token': token, // ignore: use_null_aware_elements
'cipher': 'true',
'type': 'mode2',
};
return uri.replace(queryParameters: params).toString();
},
onEncryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息加密回调
onDecryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息解密回调
// SocketClient 内部重连(心跳超时 / stream onDone前调用
onBeforeReconnect: () =>
_proactiveTokenRefresh(apiConfig, logTag: 'Socket'),
onLog: _makeLogger('Socket'),
onCheckNetworkAvailable: _checkNetwork(networkMonitor),
);
});
/// SocketClient Provider内部使用不对外暴露
///
/// 与 networkSdkApiProvider 对称。
final _socketClientProvider = Provider<NetworksMessagingApi>((ref) {
final config = ref.read(_socketConfigProvider);
return NetworksMessagingApi()..initialize(config);
});
/// SocketManager Provider
///
/// 封装连接生命周期、网络/前后台事件响应、操作前置检查、消息预处理。
/// 业务模块通过此 Provider 访问 WebSocket 能力。
///
/// ## 前置检查
///
/// connect / send 前先检查网络可用性 + 后台状态,
/// 无效操作直接跳过,避免无意义的网络请求。
/// 与 HTTP 层 [ApiClient.executeRequest] 的网络前置检查对称。
///
/// ## 事件驱动
///
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
/// 自动触发断连/重连。
///
/// Token 更新由 [_tokenUpdateStreamProvider] 事件流驱动,
/// HTTP 层刷新 token 后自动同步到 WebSocket。
///
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
/// 后续接入加解密 SDK 时,在此注入解密回调,
/// SDK 内部不调用其他 SDK。
final socketManagerProvider = Provider<SocketManager>((ref) {
final client = ref.read(_socketClientProvider);
final networkMonitor = ref.read(networkMonitorProvider);
final apiConfig = ref.read(apiConfigProvider);
final tokenStream = ref.read(_tokenUpdateStreamProvider);
final manager = SocketManager(
client: client,
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
disconnectInBackground: false, // 所有平台后台保活,心跳不停、连接不断
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
// SocketManager 层重连(前台恢复 / 网络恢复)前调用
onBeforeReconnect: () =>
_proactiveTokenRefresh(apiConfig, logTag: 'SocketManager'),
onCheckNetworkAvailable: _checkNetwork(networkMonitor),
onLog: _makeLogger('SocketManager'),
);
// 监听 token 更新事件 → 同步到 WebSocket
final tokenSub = tokenStream.stream.listen((newToken) {
manager.updateToken(newToken);
});
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
final networkSub = networkMonitor.onStatusChanged.listen((isAvailable) {
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
});
ref.onDispose(() {
tokenSub.cancel();
networkSub.cancel();
unawaited(manager.dispose());
});
return manager;
});
// ── 辅助 ──────────────────────────────────────────────────────────────────────
/// 日志回调工厂,各模块传自己的默认 tag
///
/// SDK 内部调用 onLog 时通常已传 tagdefaultTag 仅作兜底。
OnLog _makeLogger(String defaultTag) => (message, {tag}) {
debugPrint('[${tag ?? defaultTag}] $message');
};
/// 网络可用性检查回调HTTP 和 WebSocket 共用
OnCheckNetworkAvailable _checkNetwork(NetworkMonitor monitor) =>
() async => monitor.isConnected;
/// 重连前主动刷新 token距过期不足阈值时提前刷新
///
/// 两处调用:
/// - SocketClient 内部重连(心跳超时 / stream onDone
/// - SocketManager 重连(前台恢复 / 网络恢复)前
///
/// 刷新后通过 onTokenUpdated → sync stream → socketClient._currentToken 同步更新,
/// 确保随后的 _doConnect() 使用新 token。
Future<void> _proactiveTokenRefresh(
ApiConfig config, {
required String logTag,
}) async {
final currentToken = config.token;
if (currentToken == null || config.onGetTokenExpiry == null) return;
final expiry = config.onGetTokenExpiry!(currentToken);
if (expiry == null) return;
final remaining = expiry.difference(DateTime.now());
if (remaining > config.proactiveRefreshThreshold) return;
debugPrint(
'[$logTag] Token expiring in ${remaining.inMinutes}min, refreshing before reconnect',
);
final newToken = await config.onTokenRefresh?.call();
if (newToken != null && newToken.isNotEmpty) {
config.updateToken(newToken);
}
}
/// HTTP baseURL → WebSocket URL 转换
///
/// https://api.example.com → wss://api.example.com/ws
/// http://api.example.com → ws://api.example.com/ws
String _buildWsUrl(String httpBaseUrl) {
String base = httpBaseUrl;
if (base.startsWith('https://')) {
base = base.replaceFirst('https://', 'wss://');
} else if (base.startsWith('http://')) {
base = base.replaceFirst('http://', 'ws://');
}
return '$base${ApiPaths.wsConnect}';
}
// ══════════════════════════════════════════════════════════════════════════════
// 本文件的职责
// ══════════════════════════════════════════════════════════════════════════════
//
// 提供所有网络基础设施 Provider网络监听 + HTTP + WebSocket。
// 业务模块的 DI 链路Repository → UseCase 按需)
// 内聚在 features/{模块}/di/{模块}_providers.dart 中。
//
// di/ 目录只放「需要手动装配的 Provider」构造注入、回调组合等
// ViewModel Provider 由 @riverpod 注解自动生成,不在 di/ 中。
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ DI 架构 │
// ├──────────────────────────────────────────────────────────────────────────┤
// │ app/di/ ← 手动装配SDK 基础设施 │
// │ ├── app_providers.dart → 主题 + 启动初始化 │
// │ └── network_provider.dart → 网络监听 + HTTP + WebSocket │
// │ │
// │ features/{模块}/di/ ← 手动装配:业务模块 DI 链路 │
// │ └── auth_providers.dart → Repository → UseCase按需
// │ chat_providers.dart 每个模块 DI 链路内聚在一个文件 │
// │ │
// │ features/{模块}/presentation/ ← @riverpod 自动生成ViewModel │
// │ └── login_view_model.dart → loginViewModelProvider代码生成
// └──────────────────────────────────────────────────────────────────────────┘
//
// Provider 链路:
//
// networkMonitorProvider公共服务HTTP + WS 共用)
// ├── apiConfigProvider → networkSdkApiProvider ← HTTP 层
// └── _socketConfigProvider → _socketClientProvider ← WS 层(内部)
// → socketManagerProvider
//
// _tokenUpdateStreamProvider打破循环引用的中间层
// ← apiConfigProvider.onTokenUpdated 推送
// → socketManagerProvider 监听 → socketManager.updateToken()
//
// 网络事件驱动链路:
//
// connectivity_plus平台网络事件
// → NetworkMonitor.onStatusChangedtrue / false
// → SocketManager.handleNetworkStatusChanged()
// → 断网: disconnect()
// → 恢复: onBeforeReconnect → connect(token: lastToken)
//
// 前后台事件驱动链路:
//
// WidgetsBindingObserverApp 层 app.dart
// → SocketManager.onEnterBackground()
// disconnectInBackground=false → 完全保活,心跳不停(本项目默认)
// disconnectInBackground=true → disconnect + 暂停心跳(省电模式)
// → SocketManager.onEnterForeground()
// 保活模式 → 检查连接健康,异常则重连
// 断连模式 → onBeforeReconnect → reconnect
//
// Token 刷新 → WebSocket 同步链路:
//
// RetryInterceptor 检测 token 过期
// → TokenRefreshManager.refreshIfNeeded()
// → apiConfig.updateToken(newToken)
// → onTokenUpdated(newToken)
// → _tokenUpdateStream.add(newToken)
// → socketManager.updateToken(newToken) // 不断连,下次重连自动用新 token
//
// 主动 token 刷新(重连前,两个层级):
//
// SocketManager 层(前台恢复 / 网络恢复触发):
// SocketManager.onBeforeReconnect()
// → 解析 JWT exp → 距过期 < 阈值
// → apiConfig.onTokenRefresh() → 刷新
// → apiConfig.updateToken(newToken)
// → sync stream → manager.updateToken → _lastToken 更新
// → _client.connect(token: _lastToken) 使用新 token
//
// SocketClient 层(心跳超时 / stream onDone 触发):
// SocketConfig.onBeforeReconnect()
// → 同上逻辑:检查 JWT exp → 刷新 → apiConfig.updateToken
// → sync stream → manager.updateToken → _client.updateToken
// → socketClient._currentToken 同步更新
// → _doConnect() 使用新 token
//
// Repository 直接注入 ApiClient通过回调注入其他 SDK 能力:
//
// onTokenUpdate: (token) {
// apiConfig.updateToken(token); // 内存network_sdk
// secureStorage.saveToken(token); // 持久化crypto_sdk
// }
//
// 这样 network_sdk 和 crypto_sdk 互不依赖App 层是唯一知道两者的地方。
//
// ══════════════════════════════════════════════════════════════════════════════
// 新增接口的完整流程(以登录为例)
// ══════════════════════════════════════════════════════════════════════════════
//
// 「一个接口 = 一个 Request 文件」,严格按层调用,禁止跳层。
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ 文件 & 职责总览 │
// ├──────────────────────────────────────────────────────────────────────────┤
// │ login_request.dart Request + Response DTO一个端点一个文件
// │ auth_repository_impl.dart executeRequest → DTO → Entity + 回调写 Token│
// │ login_usecase.dart 格式校验 → 调 Repository按需非必须
// │ auth_providers.dart DI 装配Repository → UseCase 按需) │
// │ login_view_model.dart ref.read(authRepositoryProvider).login() │
// │ 或 ref.read(loginUseCaseProvider).execute() │
// │ login_page.dart ref.watch(loginViewModelProvider) │
// └──────────────────────────────────────────────────────────────────────────┘
//
// ─────────────────────────────────────────────────────────────────────────
// Step 1: 定义 Requestdata/remote/login_request.dart
// ─────────────────────────────────────────────────────────────────────────
//
// 一个文件包含两部分Response DTO + Request 类。
//
// @JsonSerializable()
// class LoginData { // Response DTO
// final String token;
// final String userId;
// factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
// User toEntity() => User(id: userId, ...); // DTO → Domain Entity
// }
//
// @ApiRequest(path: ApiPaths.authLogin, method: HttpMethod.post,
// responseType: LoginData, requestType: ApiRequestType.login)
// @JsonSerializable()
// class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
// final String email;
// final String password;
// LoginRequest({required this.email, required this.password});
// @override Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
// }
//
// build_runner 生成 _$LoginRequestApi mixin → 自动提供 path / method / fromJson 注册。
//
// ─────────────────────────────────────────────────────────────────────────
// Step 2: Repositorydata/repositories/auth_repository_impl.dart
// ─────────────────────────────────────────────────────────────────────────
//
// class AuthRepositoryImpl implements AuthRepository {
// final ApiClient _client; // ← 直接注入 ApiClient
// final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
//
// Future<User> login({required String email, required String password}) async {
// final dto = await _client.executeRequest(
// LoginRequest(email: email, password: password),
// );
// _onTokenUpdate(dto!.token); // 回调写入 Token
// return dto.toEntity(); // DTO → Domain Entity
// }
// }
//
// ─────────────────────────────────────────────────────────────────────────
// Step 3: Provider 装配 + ViewModel
// ─────────────────────────────────────────────────────────────────────────
//
// // --- Provider 装配features/auth/di/auth_providers.dart ---
//
// // Repository直接注入 ApiClient + 回调组合多个 SDK 能力)
// final authRepositoryProvider = Provider((ref) {
// final apiConfig = ref.read(apiConfigProvider);
// return AuthRepositoryImpl(
// client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
// onTokenUpdate: (token) {
// apiConfig.updateToken(token); // 内存network_sdk
// // secureStorage.saveToken(token); // 持久化crypto_sdk
// },
// );
// });
//
// // UseCase按需 — 登录有多步编排,所以需要)
// final loginUseCaseProvider = Provider((ref) {
// return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
// });
//
// // --- ViewModelfeatures/auth/presentation/login_view_model.dart ---
//
// // 常规写法ViewModel 直接调 Repository
// @riverpod
// class LoginViewModel extends _$LoginViewModel {
// Future<void> login(String email, String password) async {
// state = state.copyWith(isLoading: true);
// try {
// final user = await ref.read(authRepositoryProvider).login(
// email: email, password: password,
// );
// state = state.copyWith(user: user, isLoading: false);
// } on ApiError catch (e) {
// state = state.copyWith(error: e.displayMessage, isLoading: false);
// }
// }
// }
//
// // 进阶写法:有 UseCase 时(格式校验 + 多步编排)
// // final user = await ref.read(loginUseCaseProvider).execute(
// // email: email, password: password,
// // );
//
// ═════════════════════════════════════════════════════════════════════════
// 内部执行链路(点击登录按钮后发生了什么)
// ═════════════════════════════════════════════════════════════════════════
//
// View: vm.login(email, password)
// → ViewModel: ref.read(authRepositoryProvider).login(...) ← 常规路径
// → ViewModel: ref.read(loginUseCaseProvider).execute(...) ← 进阶路径(有 UseCase 时)
// → UseCase: 格式校验(邮箱 + 密码)
// → UseCase/ViewModel: authRepository.login(...)
// → Repository: _client.executeRequest(LoginRequest(...))
// → ApiClient.executeRequest(request)
// 1. 拼 URL: baseURL + "/auth/login"
// 2. request.parameters 触发 fromJson 自动注册
// 3. AuthInterceptor: 注入 token + platform headers
// 4. Dio.request(url, data: {email, password})
// 5. RetryInterceptor: token 过期 → 刷新 → 自动重试
// 6. LoggingInterceptor: 打印请求/响应日志
// → request.decodeResponse(response)
// 1. ApiResponseWrapper.fromJson: 拆 { code, message, data }
// 2. 检查 code != 0 → 抛 ApiError
// 3. fromJsonRegistry[LoginData] → LoginData.fromJson(data)
// → 返回 LoginDataDTO
// → _onTokenUpdate(token) 回调写入 TokenProvider 层组合:内存 + 持久化)
// → LoginData.toEntity() → UserDomain Entity
// → state.copyWith(user: user) 更新状态
// View: ref.watch → 自动 rebuild UI
//
// ═════════════════════════════════════════════════════════════════════════
// 各 HTTP 方法速查 — 新增接口时参照
// ═════════════════════════════════════════════════════════════════════════
//
// GET参数走 URL query string
// @ApiRequest(path: ..., method: HttpMethod.get, responseType: ProfileData)
// class GetProfileRequest extends ApiRequestable<ProfileData> with _$... { }
//
// POST参数走 JSON body
// 见上方 LoginRequest 示例。
//
// DELETE / PUT / PATCH与 POST 相同,只改 method
// @ApiRequest(path: ..., method: HttpMethod.delete, responseType: ...)
//
// POST 无响应数据(如 logout
// class LogoutRequest extends ApiRequestable<void> { ... }
// // → 返回 null
//
// Upload A: FormData 上传到自有后端
// @override Object? get uploadData => FormData.fromMap({ 'file': ... });
//
// Upload B: 二进制上传到 S3 presigned URL
// @override String get path => presignedURL; // 完整 URL不拼 baseURL
// @override Object? get uploadData => bytes; // Uint8List
// @override decodeResponse(response) { ... } // S3 不走标准响应格式
//