@@ -1,15 +1,17 @@
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 ' ../.. /core/foundation/api_paths.dart' ;
import ' ../.. /core/foundation/config.dart' ;
import ' ../.. /core/foundation/constants.dart' ;
import ' ../.. /core/foundation/errors .dart' ;
import ' ../.. /core/foundation/util s.dart' ;
import ' ../../core/services/network_monitor .dart' ;
import ' ../.. /core/services/socket_manage r.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/error s.dart' ;
import ' package:im_app/core/foundation/utils .dart' ;
import ' package:im_app /core/services/network_monito r.dart' ;
import ' package:im_app/core/services/socket_manager.dart ' ;
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
@@ -35,12 +37,7 @@ import '../../core/services/socket_manager.dart';
/// });
/// ```
final networkMonitorProvider = Provider < NetworkMonitor > ( ( ref ) {
final monitor = NetworkMonitor (
onLog: ( message , { tag } ) {
// ignore: avoid_print
print ( ' [ ${ tag ? ? ' Network ' } ] $ message ' ) ;
} ,
) ;
final monitor = NetworkMonitor ( onLog: _makeLogger ( ' Network ' ) ) ;
ref . onDispose ( ( ) {
monitor . dispose ( ) ;
@@ -80,15 +77,13 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
return ApiConfig (
baseURL: AppConfig . apiBaseUrl ,
platformHeaders: {
' P latform' : ' Android ' , // TODO: 运行时从 platform API 获取
' client-version ' : ' 1.0.0 ' , // TODO: 运行时从 package_info 获取
' Channel ' : ' ' , // TODO: 从 AppConfig 读取渠道标识
' lang ' : ' zh-CN ' , // TODO: 从 l10n_sdk 或系统 locale 动态获取
} ,
tokenExpiredCodes: ApiErrorCodes . tokenExpiredCodes ,
forceLogoutCodes: ApiErrorCodes . forceLogoutCodes ,
onForceLogout: ( ) {
// TODO: 清除登录态,跳转登录页
' p latform' : 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 逻辑
@@ -98,7 +93,7 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
// 通过事件流同步到 WebSocket, 避免直接引用 socketManagerProvider 造成循环依赖
tokenStream . add ( newToken ) ;
} ,
onCheckNetworkAvailable: ( ) async = > networkMonitor . isConnected ,
onCheckNetworkAvailable: _checkNetwork ( networkMonitor ) ,
// TODO: 接入 cipher_guard_sdk 后注入请求加密回调。
// 前提: AuthNotifier.login() 中已完成 cipherSdk.setActiveKeyPair(pub, priv)。
// 示例:
@@ -117,16 +112,43 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
// return jsonDecode(plaintext) as Map<String, dynamic>;
// },
onDecryptResponse: null ,
onBusinessError: null , // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
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: ( messa ge, { tag } ) {
// ignore: avoid_print
print ( ' [ ${ tag ? ? ' Network ' } ] $ message ' ) ;
} ,
onLog: _makeLog ger ( ' Network ' ) ,
) ;
} ) ;
@@ -146,48 +168,30 @@ final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
/// 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 场景始终保持连接
onBuildConnectUrl:
null , // TODO: 接入 cipher_guard_sdk 后注入 WS URL 加密(路径/token/cipher 参数)
// 接入 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 后注入消息解密回调
onBeforeReconnect: ( ) async {
// SocketClient 内部重连( 心跳超时、stream onDone) 前调用。
// 与 SocketManager.onB efo reReconnect 职责相同:检查 token 并按需刷新。
// 刷新后通过 sync stream 同步传播到 SocketClient._currentToken,
// 确保随后的 _doConnect() 使用新 token。
final apiConfig = ref . read ( apiConfigProvider ) ;
final currentToken = apiConfig . token ;
if ( currentToken = = null | | apiConfig . onGetTokenExpiry = = null ) return ;
final expiry = apiConfig . onGetTokenExpiry ! ( currentToken ) ;
if ( expiry = = null ) return ;
final remaining = expiry . difference ( DateTime . now ( ) ) ;
if ( remaining > apiConfig . proactiveRefreshThreshold ) return ;
// ignore: avoid_print
print (
' [Socket] Token expiring in ${ remaining . inMinutes } min, refreshing before reconnect ' ,
) ;
final newToken = await apiConfig . onTokenRefresh ? . call ( ) ;
if ( newToken ! = null & & newToken . isNotEmpty ) {
// updateToken → onTokenUpdated → sync stream → manager.updateToken
// → _client.updateToken → socketClient._currentToken 同步更新
apiConfig . updateToken ( newToken ) ;
}
} ,
onLog: ( message , { tag } ) {
// ignore: avoid_print
print ( ' [ ${ tag ? ? ' Socket ' } ] $ message ' ) ;
} ,
onCheckNetworkAvailable: ( ) async {
return networkMonitor . isConnected ;
} ,
// SocketClient 内部重连(心跳超时 / stream onDone) 前调用
onBeforeReconnect: ( ) = >
_proactiveTokenR efresh ( apiConfig , logTag: ' Socket ' ) ,
onLog: _makeLogger ( ' Socket ' ) ,
onCheckNetworkAvailable: _checkNetwork ( networkMonitor ) ,
) ;
} ) ;
@@ -232,32 +236,11 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
wsUrl: _buildWsUrl ( AppConfig . apiBaseUrl ) ,
disconnectInBackground: false , // 所有平台后台保活,心跳不停、连接不断
onMessageTransform: null , // TODO: 接入加解密 SDK 后注入解密回调
onBeforeReconnect: ( ) async {
// 重连前检查 token 是否即将过期,是则主动刷新
final currentToken = apiConfig . token ;
if ( currentToken = = null | | apiConfig . onGetTokenExpiry = = null ) return ;
final expiry = apiConfig . onGetTokenExpiry ! ( currentToken ) ;
if ( expiry = = null ) return ;
final remaining = expiry . difference ( DateTime . now ( ) ) ;
if ( remaining > apiConfig . proactiveRefreshThreshold ) return ;
// ignore: avoid_print
print (
' [SocketManager] Token expiring in ${ remaining . inMinutes } min, refreshing before reconnect ' ,
) ;
final newToken = await apiConfig . onTokenRefresh ? . call ( ) ;
if ( newToken ! = null & & newToken . isNotEmpty ) {
// updateToken 触发 onTokenUpdated → tokenStream → socketManager.updateToken
apiConfig . updateToken ( newToken ) ;
}
} ,
onCheckNetworkAvailable: ( ) async = > networkMonitor . isConnected ,
onLog: ( message , { tag } ) {
// ignore: avoid_print
print ( ' [ ${ tag ? ? ' SocketManager ' } ] $ message ' ) ;
} ,
// SocketManager 层重连(前台恢复 / 网络恢复)前调用
onBeforeReconnect: ( ) = >
_proactiveTokenRefresh ( apiConfig , logTag: ' SocketManager ' ) ,
onCheckNetworkAvailable: _checkNetwork ( networkMonitor ) ,
onLog: _makeLogger ( ' SocketManager ' ) ,
) ;
// 监听 token 更新事件 → 同步到 WebSocket
@@ -281,6 +264,47 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
// ── 辅助 ──────────────────────────────────────────────────────────────────────
/// 日志回调工厂,各模块传自己的默认 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 < 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
@@ -392,9 +416,9 @@ String _buildWsUrl(String httpBaseUrl) {
// 「一个接口 = 一个 Request 文件」,严格按层调用,禁止跳层。
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ 文件 & 职责总览 │
// │ 文件 & 职责总览 │
// ├──────────────────────────────────────────────────────────────────────────┤
// │ login_request.dart Request + Response DTO( 一个端点一个文件) │
// │ login_request.dart Request + Response DTO( 一个端点一个文件) │
// │ auth_repository_impl.dart executeRequest → DTO → Entity + 回调写 Token│
// │ login_usecase.dart 格式校验 → 调 Repository( 按需, 非必须) │
// │ auth_providers.dart DI 装配( Repository → UseCase 按需) │