529 lines
26 KiB
Dart
529 lines
26 KiB
Dart
import 'dart:async';
|
||
|
||
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';
|
||
|
||
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
|
||
|
||
/// 网络状态监听 Provider(全局单例)
|
||
///
|
||
/// 基于 connectivity_plus 监听平台网络变化,
|
||
/// 作为公共服务供多个模块使用:
|
||
/// - SocketManager:网络变化时自动断连/重连 WebSocket
|
||
/// - HTTP 层:请求前检查网络可用性
|
||
/// - UI 层:显示网络状态提示
|
||
///
|
||
/// ## 使用
|
||
///
|
||
/// ```dart
|
||
/// // 查询当前状态
|
||
/// final isConnected = ref.read(networkMonitorProvider).isConnected;
|
||
///
|
||
/// // 监听状态变化
|
||
/// ref.listen(networkMonitorProvider, (prev, monitor) {
|
||
/// monitor.onStatusChanged.listen((isAvailable) {
|
||
/// // 处理网络变化
|
||
/// });
|
||
/// });
|
||
/// ```
|
||
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||
final monitor = NetworkMonitor(
|
||
onLog: (message, {tag}) {
|
||
// ignore: avoid_print
|
||
print('[${tag ?? 'Network'}] $message');
|
||
},
|
||
);
|
||
|
||
ref.onDispose(() {
|
||
monitor.dispose();
|
||
});
|
||
|
||
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(全局单例)
|
||
///
|
||
/// 从 [AppConfig.apiBaseUrl](config.json → --dart-define-from-file)读取 baseURL,
|
||
/// 注入到 Network SDK 作为所有 HTTP 请求的基础 URL。
|
||
///
|
||
/// [onCheckNetworkAvailable] 由 [networkMonitorProvider](公共服务)注入,
|
||
/// 请求前先判断网络状态,无网络时直接抛 [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: 运行时从 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: 清除登录态,跳转登录页
|
||
},
|
||
onTokenRefresh: () async {
|
||
// TODO: App 层刷新 token 逻辑
|
||
return null;
|
||
},
|
||
onTokenUpdated: (newToken) {
|
||
// 通过事件流同步到 WebSocket,避免直接引用 socketManagerProvider 造成循环依赖
|
||
tokenStream.add(newToken);
|
||
},
|
||
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||
onEncryptRequest: null, // TODO: 接入 cipher_guard_sdk 后注入请求加密回调
|
||
onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调
|
||
onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
|
||
onTransformResponse:
|
||
null, // TODO: 如后端信封结构非标准,在此归一化为 { code, data, message }
|
||
onGetTokenExpiry: parseJwtExpiry,
|
||
maxRetries: AppConstants.maxRetries,
|
||
retryBaseDelay: AppConstants.retryBaseDelay,
|
||
onLog: (message, {tag}) {
|
||
// ignore: avoid_print
|
||
print('[${tag ?? 'Network'}] $message');
|
||
},
|
||
);
|
||
});
|
||
|
||
/// API 客户端 Provider(全局单例)
|
||
///
|
||
/// 含拦截器(Auth / Retry / Logging)、超时配置。
|
||
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||
final config = ref.read(apiConfigProvider);
|
||
return NetworksSdkApi()..initialize(config);
|
||
});
|
||
|
||
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
||
|
||
/// SocketConfig Provider(内部使用,不对外暴露)
|
||
///
|
||
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
||
/// SDK 内部不调用其他 SDK。
|
||
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');
|
||
},
|
||
onCheckNetworkAvailable: () async {
|
||
return networkMonitor.isConnected;
|
||
},
|
||
);
|
||
});
|
||
|
||
/// SocketClient Provider(内部使用,不对外暴露)
|
||
///
|
||
/// 与 networkSdkApiProvider 对称。
|
||
final _socketClientProvider = Provider<NetworksMessagingApi>((ref) {
|
||
final config = ref.read(_socketConfigProvider);
|
||
return NetworksMessagingApi()..initialize(config);
|
||
});
|
||
|
||
/// SocketManager Provider
|
||
///
|
||
/// 封装连接生命周期、网络/前后台事件响应、操作前置检查、消息预处理。
|
||
/// 业务模块通过此 Provider 访问 WebSocket 能力。
|
||
///
|
||
/// ## 前置检查
|
||
///
|
||
/// connect / send 前先检查网络可用性 + 后台状态,
|
||
/// 无效操作直接跳过,避免无意义的网络请求。
|
||
/// 与 HTTP 层 [ApiClient.executeRequest] 的网络前置检查对称。
|
||
///
|
||
/// ## 事件驱动
|
||
///
|
||
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
||
/// 自动触发断连/重连。
|
||
///
|
||
/// Token 更新由 [_tokenUpdateStreamProvider] 事件流驱动,
|
||
/// HTTP 层刷新 token 后自动同步到 WebSocket。
|
||
///
|
||
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
||
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
||
/// SDK 内部不调用其他 SDK。
|
||
final socketManagerProvider = Provider<SocketManager>((ref) {
|
||
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),
|
||
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');
|
||
},
|
||
);
|
||
|
||
// 监听 token 更新事件 → 同步到 WebSocket
|
||
final tokenSub = tokenStream.stream.listen((newToken) {
|
||
manager.updateToken(newToken);
|
||
});
|
||
|
||
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
||
final networkSub = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
||
});
|
||
|
||
ref.onDispose(() {
|
||
tokenSub.cancel();
|
||
networkSub.cancel();
|
||
unawaited(manager.dispose());
|
||
});
|
||
|
||
return manager;
|
||
});
|
||
|
||
// ── 辅助 ──────────────────────────────────────────────────────────────────────
|
||
|
||
/// HTTP baseURL → WebSocket URL 转换
|
||
///
|
||
/// https://api.example.com → wss://api.example.com/ws
|
||
/// http://api.example.com → ws://api.example.com/ws
|
||
String _buildWsUrl(String httpBaseUrl) {
|
||
String base = httpBaseUrl;
|
||
if (base.startsWith('https://')) {
|
||
base = base.replaceFirst('https://', 'wss://');
|
||
} else if (base.startsWith('http://')) {
|
||
base = base.replaceFirst('http://', 'ws://');
|
||
}
|
||
return '$base${ApiPaths.wsConnect}';
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// 本文件的职责
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
//
|
||
// 提供所有网络基础设施 Provider:网络监听 + HTTP + WebSocket。
|
||
// 业务模块的 DI 链路(Repository → UseCase 按需)
|
||
// 内聚在 features/{模块}/di/{模块}_providers.dart 中。
|
||
//
|
||
// di/ 目录只放「需要手动装配的 Provider」(构造注入、回调组合等)。
|
||
// ViewModel Provider 由 @riverpod 注解自动生成,不在 di/ 中。
|
||
//
|
||
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||
// │ DI 架构 │
|
||
// ├──────────────────────────────────────────────────────────────────────────┤
|
||
// │ app/di/ ← 手动装配:SDK 基础设施 │
|
||
// │ ├── app_providers.dart → 主题 + 启动初始化 │
|
||
// │ └── network_provider.dart → 网络监听 + HTTP + WebSocket │
|
||
// │ │
|
||
// │ features/{模块}/di/ ← 手动装配:业务模块 DI 链路 │
|
||
// │ └── auth_providers.dart → Repository → UseCase(按需) │
|
||
// │ chat_providers.dart 每个模块 DI 链路内聚在一个文件 │
|
||
// │ │
|
||
// │ features/{模块}/presentation/ ← @riverpod 自动生成:ViewModel │
|
||
// │ └── login_view_model.dart → loginViewModelProvider(代码生成) │
|
||
// └──────────────────────────────────────────────────────────────────────────┘
|
||
//
|
||
// Provider 链路:
|
||
//
|
||
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
||
// ├── apiConfigProvider → networkSdkApiProvider ← HTTP 层
|
||
// └── _socketConfigProvider → _socketClientProvider ← WS 层(内部)
|
||
// → socketManagerProvider
|
||
//
|
||
// _tokenUpdateStreamProvider(打破循环引用的中间层)
|
||
// ← apiConfigProvider.onTokenUpdated 推送
|
||
// → socketManagerProvider 监听 → socketManager.updateToken()
|
||
//
|
||
// 网络事件驱动链路:
|
||
//
|
||
// connectivity_plus(平台网络事件)
|
||
// → NetworkMonitor.onStatusChanged(true / false)
|
||
// → SocketManager.handleNetworkStatusChanged()
|
||
// → 断网: disconnect()
|
||
// → 恢复: onBeforeReconnect → connect(token: lastToken)
|
||
//
|
||
// 前后台事件驱动链路:
|
||
//
|
||
// WidgetsBindingObserver(App 层 app.dart)
|
||
// → SocketManager.onEnterBackground()
|
||
// disconnectInBackground=true → disconnect(默认,移动端省电)
|
||
// disconnectInBackground=false → 完全保活,不断连不暂停心跳(桌面端)
|
||
// → 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 能力:
|
||
//
|
||
// onTokenUpdate: (token) {
|
||
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||
// secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||
// }
|
||
//
|
||
// 这样 network_sdk 和 crypto_sdk 互不依赖,App 层是唯一知道两者的地方。
|
||
//
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// 新增接口的完整流程(以登录为例)
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
//
|
||
// 「一个接口 = 一个 Request 文件」,严格按层调用,禁止跳层。
|
||
//
|
||
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||
// │ 文件 & 职责总览 │
|
||
// ├──────────────────────────────────────────────────────────────────────────┤
|
||
// │ login_request.dart Request + Response DTO(一个端点一个文件) │
|
||
// │ auth_repository_impl.dart executeRequest → DTO → Entity + 回调写 Token│
|
||
// │ login_usecase.dart 格式校验 → 调 Repository(按需,非必须) │
|
||
// │ auth_providers.dart DI 装配(Repository → UseCase 按需) │
|
||
// │ login_view_model.dart ref.read(authRepositoryProvider).login() │
|
||
// │ 或 ref.read(loginUseCaseProvider).execute() │
|
||
// │ login_page.dart ref.watch(loginViewModelProvider) │
|
||
// └──────────────────────────────────────────────────────────────────────────┘
|
||
//
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Step 1: 定义 Request(data/remote/login_request.dart)
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
//
|
||
// 一个文件包含两部分:Response DTO + Request 类。
|
||
//
|
||
// @JsonSerializable()
|
||
// class LoginData { // Response DTO
|
||
// final String token;
|
||
// final String userId;
|
||
// factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
|
||
// User toEntity() => User(id: userId, ...); // DTO → Domain Entity
|
||
// }
|
||
//
|
||
// @ApiRequest(path: ApiPaths.authLogin, method: HttpMethod.post,
|
||
// responseType: LoginData, requestType: ApiRequestType.login)
|
||
// @JsonSerializable()
|
||
// class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||
// final String email;
|
||
// final String password;
|
||
// LoginRequest({required this.email, required this.password});
|
||
// @override Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||
// }
|
||
//
|
||
// build_runner 生成 _$LoginRequestApi mixin → 自动提供 path / method / fromJson 注册。
|
||
//
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Step 2: Repository(data/repositories/auth_repository_impl.dart)
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
//
|
||
// class AuthRepositoryImpl implements AuthRepository {
|
||
// final ApiClient _client; // ← 直接注入 ApiClient
|
||
// final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
|
||
//
|
||
// Future<User> login({required String email, required String password}) async {
|
||
// final dto = await _client.executeRequest(
|
||
// LoginRequest(email: email, password: password),
|
||
// );
|
||
// _onTokenUpdate(dto!.token); // 回调写入 Token
|
||
// return dto.toEntity(); // DTO → Domain Entity
|
||
// }
|
||
// }
|
||
//
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Step 3: Provider 装配 + ViewModel
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
//
|
||
// // --- Provider 装配(features/auth/di/auth_providers.dart) ---
|
||
//
|
||
// // Repository(直接注入 ApiClient + 回调组合多个 SDK 能力)
|
||
// final authRepositoryProvider = Provider((ref) {
|
||
// final apiConfig = ref.read(apiConfigProvider);
|
||
// return AuthRepositoryImpl(
|
||
// client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||
// onTokenUpdate: (token) {
|
||
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||
// },
|
||
// );
|
||
// });
|
||
//
|
||
// // UseCase(按需 — 登录有多步编排,所以需要)
|
||
// final loginUseCaseProvider = Provider((ref) {
|
||
// return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
||
// });
|
||
//
|
||
// // --- ViewModel(features/auth/presentation/login_view_model.dart) ---
|
||
//
|
||
// // 常规写法:ViewModel 直接调 Repository
|
||
// @riverpod
|
||
// class LoginViewModel extends _$LoginViewModel {
|
||
// Future<void> login(String email, String password) async {
|
||
// state = state.copyWith(isLoading: true);
|
||
// try {
|
||
// final user = await ref.read(authRepositoryProvider).login(
|
||
// email: email, password: password,
|
||
// );
|
||
// state = state.copyWith(user: user, isLoading: false);
|
||
// } on ApiError catch (e) {
|
||
// state = state.copyWith(error: e.displayMessage, isLoading: false);
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// // 进阶写法:有 UseCase 时(格式校验 + 多步编排)
|
||
// // final user = await ref.read(loginUseCaseProvider).execute(
|
||
// // email: email, password: password,
|
||
// // );
|
||
//
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// 内部执行链路(点击登录按钮后发生了什么)
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
//
|
||
// View: vm.login(email, password)
|
||
// → ViewModel: ref.read(authRepositoryProvider).login(...) ← 常规路径
|
||
// → ViewModel: ref.read(loginUseCaseProvider).execute(...) ← 进阶路径(有 UseCase 时)
|
||
// → UseCase: 格式校验(邮箱 + 密码)
|
||
// → UseCase/ViewModel: authRepository.login(...)
|
||
// → Repository: _client.executeRequest(LoginRequest(...))
|
||
// → ApiClient.executeRequest(request)
|
||
// 1. 拼 URL: baseURL + "/auth/login"
|
||
// 2. request.parameters 触发 fromJson 自动注册
|
||
// 3. AuthInterceptor: 注入 token + platform headers
|
||
// 4. Dio.request(url, data: {email, password})
|
||
// 5. RetryInterceptor: token 过期 → 刷新 → 自动重试
|
||
// 6. LoggingInterceptor: 打印请求/响应日志
|
||
// → request.decodeResponse(response)
|
||
// 1. ApiResponseWrapper.fromJson: 拆 { code, message, data }
|
||
// 2. 检查 code != 0 → 抛 ApiError
|
||
// 3. fromJsonRegistry[LoginData] → LoginData.fromJson(data)
|
||
// → 返回 LoginData(DTO)
|
||
// → _onTokenUpdate(token) 回调写入 Token(Provider 层组合:内存 + 持久化)
|
||
// → LoginData.toEntity() → User(Domain Entity)
|
||
// → state.copyWith(user: user) 更新状态
|
||
// View: ref.watch → 自动 rebuild UI
|
||
//
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// 各 HTTP 方法速查 — 新增接口时参照
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
//
|
||
// GET(参数走 URL query string)
|
||
// @ApiRequest(path: ..., method: HttpMethod.get, responseType: ProfileData)
|
||
// class GetProfileRequest extends ApiRequestable<ProfileData> with _$... { }
|
||
//
|
||
// POST(参数走 JSON body)
|
||
// 见上方 LoginRequest 示例。
|
||
//
|
||
// DELETE / PUT / PATCH(与 POST 相同,只改 method)
|
||
// @ApiRequest(path: ..., method: HttpMethod.delete, responseType: ...)
|
||
//
|
||
// POST 无响应数据(如 logout)
|
||
// class LogoutRequest extends ApiRequestable<void> { ... }
|
||
// // → 返回 null
|
||
//
|
||
// Upload A: FormData 上传到自有后端
|
||
// @override Object? get uploadData => FormData.fromMap({ 'file': ... });
|
||
//
|
||
// Upload B: 二进制上传到 S3 presigned URL
|
||
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
|
||
// @override Object? get uploadData => bytes; // Uint8List
|
||
// @override decodeResponse(response) { ... } // S3 不走标准信封
|
||
//
|