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:
Happi (哈比)
2026-03-09 15:08:45 +08:00
163 changed files with 4341 additions and 1785 deletions

View File

@@ -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());
// ── 主题 ──────────────────────────────────────────────────────────────────────

View File

@@ -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.onStatusChangedtrue / false
// → SocketManager.handleNetworkStatusChanged()
// → 断网: disconnect()
// → 恢复: connect(token: lastToken)
// → 恢复: onBeforeReconnect → connect(token: lastToken)
//
// 前后台事件驱动链路:
//
// WidgetsBindingObserverApp 层 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 不走标准响应格式
//