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((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>((ref) { final controller = StreamController.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((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; // }, 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((ref) { final config = ref.read(apiConfigProvider); return NetworksSdkApi()..initialize(config); }); // ── WebSocket 基础设施 ──────────────────────────────────────────────────────── /// SocketConfig Provider(内部使用,不对外暴露) /// /// 与 apiConfigProvider 对称,通过回调注入 App 层能力, /// SDK 内部不调用其他 SDK。 final _socketConfigProvider = Provider((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 = { ...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((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((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 时通常已传 tag,defaultTag 仅作兜底。 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 _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.onStatusChanged(true / false) // → SocketManager.handleNetworkStatusChanged() // → 断网: disconnect() // → 恢复: onBeforeReconnect → connect(token: lastToken) // // 前后台事件驱动链路: // // WidgetsBindingObserver(App 层 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: 定义 Request(data/remote/login_request.dart) // ───────────────────────────────────────────────────────────────────────── // // 一个文件包含两部分:Response DTO + Request 类。 // // @JsonSerializable() // class LoginData { // Response DTO // final String token; // final String userId; // factory LoginData.fromJson(Map 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 with _$LoginRequestApi { // final String email; // final String password; // LoginRequest({required this.email, required this.password}); // @override Map toJson() => _$LoginRequestToJson(this); // } // // build_runner 生成 _$LoginRequestApi mixin → 自动提供 path / method / fromJson 注册。 // // ───────────────────────────────────────────────────────────────────────── // Step 2: Repository(data/repositories/auth_repository_impl.dart) // ───────────────────────────────────────────────────────────────────────── // // class AuthRepositoryImpl implements AuthRepository { // final ApiClient _client; // ← 直接注入 ApiClient // final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合 // // Future 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)); // }); // // // --- ViewModel(features/auth/presentation/login_view_model.dart) --- // // // 常规写法:ViewModel 直接调 Repository // @riverpod // class LoginViewModel extends _$LoginViewModel { // Future 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) // → 返回 LoginData(DTO) // → _onTokenUpdate(token) 回调写入 Token(Provider 层组合:内存 + 持久化) // → LoginData.toEntity() → User(Domain 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 with _$... { } // // POST(参数走 JSON body) // 见上方 LoginRequest 示例。 // // DELETE / PUT / PATCH(与 POST 相同,只改 method) // @ApiRequest(path: ..., method: HttpMethod.delete, responseType: ...) // // POST 无响应数据(如 logout) // class LogoutRequest extends ApiRequestable { ... } // // → 返回 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 不走标准响应格式 //