网络请求打通,ws 打通
This commit is contained in:
@@ -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 时通常已传 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 按需) │
|
||||
|
||||
Reference in New Issue
Block a user