Merge branch 'dev' into happi/dev/database-update
# Conflicts: # apps/im_app/lib/data/models/user_dto.dart # apps/im_app/lib/data/remote/login_request.dart # apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart # apps/im_app/lib/features/chat/view/chat_db_test_page.dart # apps/im_app/lib/features/login/presentation/login_view_model.dart
This commit is contained in:
@@ -23,11 +23,18 @@ class AuthNotifier extends ChangeNotifier {
|
||||
|
||||
void login() {
|
||||
_isLoggedIn = true;
|
||||
// TODO: 接入 cipher_guard_sdk 后,在此处完成 RSA 密钥注入:
|
||||
// 1. 从安全存储(keychain / secure storage)读取公私钥对(只读一次)
|
||||
// 2. cipherSdk.setActiveKeyPair(publicKey: pubPem, privateKey: privPem)
|
||||
// 须在 notifyListeners() 之前完成,确保路由跳转后 onEncryptRequest 回调触发时密钥已就绪。
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void logout() {
|
||||
_isLoggedIn = false;
|
||||
// TODO: 接入 cipher_guard_sdk 后,退出登录时清除内存密钥:
|
||||
// cipherSdk.clearActiveKeyPair()
|
||||
// cipherSdk.clearDerivedKeyCache()
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -37,9 +44,7 @@ class AuthNotifier extends ChangeNotifier {
|
||||
/// 使用 [Provider] 持有 [AuthNotifier] 单例。
|
||||
/// go_router 通过 [GoRouter.refreshListenable] 直接监听 [AuthNotifier](ChangeNotifier),
|
||||
/// Riverpod 侧不需要响应式更新(导航由 go_router 接管)。
|
||||
final authNotifierProvider = Provider<AuthNotifier>(
|
||||
(ref) => AuthNotifier(),
|
||||
);
|
||||
final authNotifierProvider = Provider<AuthNotifier>((ref) => AuthNotifier());
|
||||
|
||||
// ── 主题 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ 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/utils.dart';
|
||||
import '../../core/services/network_monitor.dart';
|
||||
import '../../core/services/socket_manager.dart';
|
||||
|
||||
@@ -47,6 +49,21 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||
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(全局单例)
|
||||
@@ -58,15 +75,18 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
|
||||
final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||
final networkMonitor = ref.read(networkMonitorProvider);
|
||||
final tokenStream = ref.read(_tokenUpdateStreamProvider);
|
||||
|
||||
return ApiConfig(
|
||||
baseURL: AppConfig.apiBaseUrl,
|
||||
platformHeaders: {
|
||||
'Platform': 'Android', // TODO: 运行时从平台 API 获取
|
||||
'Platform': 'Android', // TODO: 运行时从 platform API 获取
|
||||
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
||||
'Channel': '', // TODO: 从 AppConfig 读取渠道标识
|
||||
'lang': 'zh-CN', // TODO: 从 l10n_sdk 或系统 locale 动态获取
|
||||
},
|
||||
tokenExpiredCodes: {30002, 30003, 30124},
|
||||
forceLogoutCodes: {30125},
|
||||
tokenExpiredCodes: ApiErrorCodes.tokenExpiredCodes,
|
||||
forceLogoutCodes: ApiErrorCodes.forceLogoutCodes,
|
||||
onForceLogout: () {
|
||||
// TODO: 清除登录态,跳转登录页
|
||||
},
|
||||
@@ -74,7 +94,33 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||
// TODO: App 层刷新 token 逻辑
|
||||
return null;
|
||||
},
|
||||
onTokenUpdated: (newToken) {
|
||||
// 通过事件流同步到 WebSocket,避免直接引用 socketManagerProvider 造成循环依赖
|
||||
tokenStream.add(newToken);
|
||||
},
|
||||
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||
// 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: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
|
||||
onTransformResponse:
|
||||
null, // TODO: 如后端响应格式非标准,在此归一化为 { code, data, message }
|
||||
onGetTokenExpiry: parseJwtExpiry,
|
||||
maxRetries: AppConstants.maxRetries,
|
||||
retryBaseDelay: AppConstants.retryBaseDelay,
|
||||
onLog: (message, {tag}) {
|
||||
@@ -94,16 +140,47 @@ final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||||
|
||||
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
||||
|
||||
/// SocketConfig Provider(全局单例)
|
||||
/// SocketConfig Provider(内部使用,不对外暴露)
|
||||
///
|
||||
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
||||
/// SDK 内部不调用其他 SDK。
|
||||
final socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
final _socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
final networkMonitor = ref.read(networkMonitorProvider);
|
||||
|
||||
return SocketConfig(
|
||||
maxReconnectAttempts: AppConstants.maxRetries,
|
||||
maxReconnectDelay: AppConstants.maxReconnectDelay,
|
||||
unlimitedReconnect: true, // IM 场景始终保持连接
|
||||
onBuildConnectUrl:
|
||||
null, // TODO: 接入 cipher_guard_sdk 后注入 WS URL 加密(路径/token/cipher 参数)
|
||||
onEncryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息加密回调
|
||||
onDecryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息解密回调
|
||||
onBeforeReconnect: () async {
|
||||
// SocketClient 内部重连(心跳超时、stream onDone)前调用。
|
||||
// 与 SocketManager.onBeforeReconnect 职责相同:检查 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');
|
||||
@@ -114,12 +191,11 @@ final socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
);
|
||||
});
|
||||
|
||||
/// SocketClient Provider(全局单例)
|
||||
/// SocketClient Provider(内部使用,不对外暴露)
|
||||
///
|
||||
/// 与 apiClientProvider 对称。
|
||||
final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
||||
{
|
||||
final config = ref.read(socketConfigProvider);
|
||||
/// 与 networkSdkApiProvider 对称。
|
||||
final _socketClientProvider = Provider<NetworksMessagingApi>((ref) {
|
||||
final config = ref.read(_socketConfigProvider);
|
||||
return NetworksMessagingApi()..initialize(config);
|
||||
});
|
||||
|
||||
@@ -139,17 +215,44 @@ final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
||||
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
||||
/// 自动触发断连/重连。
|
||||
///
|
||||
/// Token 更新由 [_tokenUpdateStreamProvider] 事件流驱动,
|
||||
/// HTTP 层刷新 token 后自动同步到 WebSocket。
|
||||
///
|
||||
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
||||
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
||||
/// SDK 内部不调用其他 SDK。
|
||||
final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||
final client = ref.read(socketClientProvider);
|
||||
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 后注入解密回调
|
||||
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
|
||||
@@ -157,13 +260,19 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 token 更新事件 → 同步到 WebSocket
|
||||
final tokenSub = tokenStream.stream.listen((newToken) {
|
||||
manager.updateToken(newToken);
|
||||
});
|
||||
|
||||
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
||||
final subscription = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||
final networkSub = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
tokenSub.cancel();
|
||||
networkSub.cancel();
|
||||
unawaited(manager.dispose());
|
||||
});
|
||||
|
||||
@@ -215,23 +324,57 @@ String _buildWsUrl(String httpBaseUrl) {
|
||||
// Provider 链路:
|
||||
//
|
||||
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
||||
// ├── apiConfigProvider → apiClientProvider ← HTTP 层
|
||||
// └── socketConfigProvider → socketClientProvider ← WS 层
|
||||
// ├── apiConfigProvider → networkSdkApiProvider ← HTTP 层
|
||||
// └── _socketConfigProvider → _socketClientProvider ← WS 层(内部)
|
||||
// → socketManagerProvider
|
||||
//
|
||||
// _tokenUpdateStreamProvider(打破循环引用的中间层)
|
||||
// ← apiConfigProvider.onTokenUpdated 推送
|
||||
// → socketManagerProvider 监听 → socketManager.updateToken()
|
||||
//
|
||||
// 网络事件驱动链路:
|
||||
//
|
||||
// connectivity_plus(平台网络事件)
|
||||
// → NetworkMonitor.onStatusChanged(true / false)
|
||||
// → SocketManager.handleNetworkStatusChanged()
|
||||
// → 断网: disconnect()
|
||||
// → 恢复: connect(token: lastToken)
|
||||
// → 恢复: onBeforeReconnect → connect(token: lastToken)
|
||||
//
|
||||
// 前后台事件驱动链路:
|
||||
//
|
||||
// WidgetsBindingObserver(App 层 app.dart)
|
||||
// → SocketManager.onEnterBackground() → disconnect
|
||||
// → SocketManager.onEnterForeground() → reconnect
|
||||
// → 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 能力:
|
||||
//
|
||||
@@ -313,7 +456,7 @@ String _buildWsUrl(String httpBaseUrl) {
|
||||
// final authRepositoryProvider = Provider((ref) {
|
||||
// final apiConfig = ref.read(apiConfigProvider);
|
||||
// return AuthRepositoryImpl(
|
||||
// client: ref.read(apiClientProvider), // 直接注入
|
||||
// client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||
// onTokenUpdate: (token) {
|
||||
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||||
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||
@@ -400,5 +543,5 @@ String _buildWsUrl(String httpBaseUrl) {
|
||||
// Upload B: 二进制上传到 S3 presigned URL
|
||||
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
|
||||
// @override Object? get uploadData => bytes; // Uint8List
|
||||
// @override decodeResponse(response) { ... } // S3 不走标准信封
|
||||
// @override decodeResponse(response) { ... } // S3 不走标准响应格式
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user