网络请求打通,ws 打通

This commit is contained in:
Cody
2026-03-09 19:05:55 +08:00
parent 997d821447
commit 3c1976b343
60 changed files with 1392 additions and 552 deletions

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/ui/base/app_theme.dart';
import 'di/app_providers.dart';
import 'di/network_provider.dart';
import 'router/app_router.dart';
import 'package:im_app/core/ui/base/app_theme.dart';
import 'package:im_app/app/di/app_providers.dart';
import 'package:im_app/app/di/network_provider.dart';
import 'package:im_app/app/router/app_router.dart';
/// 应用根组件
///

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';
import 'package:im_app/app/app.dart';
void bootstrap() {
WidgetsFlutterBinding.ensureInitialized();

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/services/app_initializer.dart';
import 'network_provider.dart';
import 'package:im_app/core/foundation/device_info.dart';
import 'package:im_app/core/services/app_initializer.dart';
import 'package:im_app/app/di/network_provider.dart';
// ── 认证 ──────────────────────────────────────────────────────────────────────
@@ -122,6 +123,11 @@ final appInitializerProvider = Provider<AppInitializer>((ref) {
name: 'NetworkMonitor',
task: () => ref.read(networkMonitorProvider).initialize(),
),
// 预取设备 ID / 设备名platformHeaders 同步读取
InitTask(
name: 'DeviceInfo',
task: DeviceInfo.init,
),
],
deferred: [
// TODO: 推送注册

View File

@@ -1,7 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:storage_sdk/storage_sdk.dart';
import '../../data/local/drift/app_database.dart';
import 'package:im_app/data/local/drift/app_database.dart';
/// 全局单例 StorageSdkApi整个 App 生命周期内唯一实例。
///

View File

@@ -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/utils.dart';
import '../../core/services/network_monitor.dart';
import '../../core/services/socket_manager.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';
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
@@ -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: {
'Platform': '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: 清除登录态,跳转登录页
'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 逻辑
@@ -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: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'Network'}] $message');
},
onLog: _makeLogger('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.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');
},
onCheckNetworkAvailable: () async {
return networkMonitor.isConnected;
},
// SocketClient 内部重连(心跳超时 / stream onDone前调用
onBeforeReconnect: () =>
_proactiveTokenRefresh(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 时通常已传 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
@@ -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 按需) │

View File

@@ -3,16 +3,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:im_app/features/chat/view/chat_db_test_page.dart';
import '../../features/app_tab/view/app_tab.dart';
import '../../features/chat/view/chat_detail_page.dart';
import '../../features/chat/view/chat_page.dart';
import '../../features/contact/view/contact_page.dart';
import '../../features/login/view/login_page.dart';
import '../../features/settings/view/settings_page.dart';
import '../../features/settings/view/theme_view.dart';
import '../di/app_providers.dart';
import 'app_route_name.dart';
import 'guards/auth_guard.dart';
import 'package:im_app/features/app_tab/view/app_tab.dart';
import 'package:im_app/features/chat/view/chat_detail_page.dart';
import 'package:im_app/features/chat/view/chat_page.dart';
import 'package:im_app/features/contact/view/contact_page.dart';
import 'package:im_app/features/login/view/login_page.dart';
import 'package:im_app/features/settings/view/settings_page.dart';
import 'package:im_app/features/settings/view/theme_view.dart';
import 'package:im_app/app/di/app_providers.dart';
import 'package:im_app/app/router/app_route_name.dart';
import 'package:im_app/app/router/guards/auth_guard.dart';
/// 应用路由 Provider
///

View File

@@ -1,7 +1,7 @@
import 'package:go_router/go_router.dart';
import '../../di/app_providers.dart';
import '../app_route_name.dart';
import 'package:im_app/app/di/app_providers.dart';
import 'package:im_app/app/router/app_route_name.dart';
/// 登录守卫
///