优化配置,修复 demo bug
1,network 框架完善 2,websocket 机制完善 3,设计文档整理到架构文档 4,脚本,配置完善
This commit is contained in:
@@ -884,12 +884,15 @@ flowchart TD
|
|||||||
│ │ ├── datasources/
|
│ │ ├── datasources/
|
||||||
│ │ │ ├── http/
|
│ │ │ ├── http/
|
||||||
│ │ │ │ ├── api_client.dart # Dio REST 客户端
|
│ │ │ │ ├── api_client.dart # Dio REST 客户端
|
||||||
|
│ │ │ │ ├── token_refresh_manager.dart # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用)
|
||||||
│ │ │ │ └── interceptor/
|
│ │ │ │ └── interceptor/
|
||||||
│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
||||||
│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
|
│ │ │ │ ├── encryption_interceptor.dart # 加密拦截器(预留给 cipher_guard_sdk)
|
||||||
|
│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试 + 业务错误钩子
|
||||||
│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
|
│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
|
||||||
│ │ │ └── socket/
|
│ │ │ ├── socket/
|
||||||
│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream)
|
│ │ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream/加密钩子)
|
||||||
|
│ │ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
|
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
|
||||||
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||||||
@@ -898,11 +901,12 @@ flowchart TD
|
|||||||
│ │ └── networks_messaging_repository_impl.dart
|
│ │ └── networks_messaging_repository_impl.dart
|
||||||
│ ├── domain/
|
│ ├── domain/
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── api_error.dart # @freezed HTTP 错误联合类型
|
│ │ │ ├── api_error.dart # @freezed HTTP 错误联合类型(7 变体)
|
||||||
|
│ │ │ ├── encrypted_request.dart # 加密请求结果数据类
|
||||||
│ │ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
│ │ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
||||||
│ │ │ ├── socket_connection_state.dart # 连接状态 enum
|
│ │ │ ├── socket_connection_state.dart # 连接状态 enum
|
||||||
│ │ │ ├── http_method.dart # GET/POST/PUT/DELETE/PATCH
|
│ │ │ ├── http_method.dart # GET/POST/PUT/DELETE/PATCH
|
||||||
│ │ │ └── api_request_type.dart # request/login/upload
|
│ │ │ └── api_request_type.dart # request/login/upload/stream/download
|
||||||
│ │ └── repositories/
|
│ │ └── repositories/
|
||||||
│ │ ├── networks_sdk_repository.dart
|
│ │ ├── networks_sdk_repository.dart
|
||||||
│ │ └── networks_messaging_repository.dart
|
│ │ └── networks_messaging_repository.dart
|
||||||
@@ -911,9 +915,9 @@ flowchart TD
|
|||||||
│ │ ├── networks_sdk_api.dart # HTTP 公开 API 接口
|
│ │ ├── networks_sdk_api.dart # HTTP 公开 API 接口
|
||||||
│ │ └── networks_messaging_api.dart # WebSocket 公开 API 接口
|
│ │ └── networks_messaging_api.dart # WebSocket 公开 API 接口
|
||||||
│ └── wiring/
|
│ └── wiring/
|
||||||
│ ├── api_config.dart # HTTP 配置(baseURL/Token/回调)
|
│ ├── api_config.dart # HTTP 配置(baseURL/Token/回调/加密/重试)
|
||||||
│ ├── socket_config.dart # WebSocket 配置(心跳/重连策略)
|
│ ├── socket_config.dart # WebSocket 配置(心跳/重连/加密钩子)
|
||||||
│ ├── network_callbacks.dart # 回调类型定义
|
│ ├── network_callbacks.dart # 回调类型定义(认证/加密/业务错误/下载/WS)
|
||||||
│ ├── networks_sdk_core.dart
|
│ ├── networks_sdk_core.dart
|
||||||
│ ├── networks_sdk_api_impl.dart
|
│ ├── networks_sdk_api_impl.dart
|
||||||
│ ├── networks_messaging_api_impl.dart
|
│ ├── networks_messaging_api_impl.dart
|
||||||
@@ -1670,7 +1674,7 @@ final viewModel = ref.read(chatViewModelProvider.notifier); // viewModel 类型
|
|||||||
<pre><code class="language-dart">// Riverpod DevTools 可以看到完整的依赖图
|
<pre><code class="language-dart">// Riverpod DevTools 可以看到完整的依赖图
|
||||||
ChatViewModel
|
ChatViewModel
|
||||||
├─ chatRepositoryProvider
|
├─ chatRepositoryProvider
|
||||||
│ ├─ apiClientProvider
|
│ ├─ networkSdkApiProvider
|
||||||
│ └─ messageLocalDataSourceProvider
|
│ └─ messageLocalDataSourceProvider
|
||||||
└─ sendMessageUseCaseProvider
|
└─ sendMessageUseCaseProvider
|
||||||
└─ chatRepositoryProvider
|
└─ chatRepositoryProvider
|
||||||
@@ -2187,8 +2191,8 @@ extension APIRequestableDefaults<T> on APIRequestable<T> {
|
|||||||
|
|
||||||
<pre><code class="language-dart">/// 执行 API 请求 - 唯一的请求入口
|
<pre><code class="language-dart">/// 执行 API 请求 - 唯一的请求入口
|
||||||
Future<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
|
Future<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
|
||||||
final dio = ref.read(apiClientProvider);
|
final client = ref.read(networkSdkApiProvider);
|
||||||
final config = ref.read(aPIConfigurationProvider);
|
final config = ref.read(apiConfigProvider);
|
||||||
|
|
||||||
// 1. 检查网络连接
|
// 1. 检查网络连接
|
||||||
if (!networkManager.isNetworkAvailable) {
|
if (!networkManager.isNetworkAvailable) {
|
||||||
@@ -2646,18 +2650,20 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 2. API 客户端(内部自动挂载 Auth / Retry / Logging 拦截器)
|
/// 2. Networks SDK API Provider(全局单例,Facade 接口)
|
||||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
|
||||||
return ApiClient(config: ref.read(apiConfigProvider));
|
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||||||
|
final config = ref.read(apiConfigProvider);
|
||||||
|
return NetworksSdkWiring.build(config: config);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)
|
// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)
|
||||||
|
|
||||||
/// 3. Repository(注入 domain 接口类型,ViewModel 不感知具体实现)
|
/// 3. Repository(注入 Facade 接口类型,ViewModel 不感知具体实现)
|
||||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||||
final apiConfig = ref.read(apiConfigProvider);
|
final apiConfig = ref.read(apiConfigProvider);
|
||||||
return AuthRepositoryImpl(
|
return AuthRepositoryImpl(
|
||||||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
onTokenUpdate: (token) {
|
onTokenUpdate: (token) {
|
||||||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||||||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||||||
@@ -2707,8 +2713,9 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
→ Repository: _client.executeRequest(LoginRequest(...))
|
→ Repository: _client.executeRequest(LoginRequest(...))
|
||||||
→ ApiClient.executeRequest() ← networks_sdk 内部
|
→ ApiClient.executeRequest() ← networks_sdk 内部
|
||||||
→ AuthInterceptor ← 注入 token + headers
|
→ AuthInterceptor ← 注入 token + headers
|
||||||
|
→ EncryptionInterceptor ← 加密请求体(预留)
|
||||||
→ Dio.request(baseURL + path, data) ← 实际 HTTP 请求
|
→ Dio.request(baseURL + path, data) ← 实际 HTTP 请求
|
||||||
→ RetryInterceptor ← token 过期自动刷新重试
|
→ RetryInterceptor ← token 过期自动刷新重试 + 业务错误钩子
|
||||||
→ LoggingInterceptor ← 请求/响应日志
|
→ LoggingInterceptor ← 请求/响应日志
|
||||||
← request.decodeResponse(response) ← 自动解码
|
← request.decodeResponse(response) ← 自动解码
|
||||||
← ApiResponseWrapper.fromJson ← 拆 { code, msg, data }
|
← ApiResponseWrapper.fromJson ← 拆 { code, msg, data }
|
||||||
@@ -2987,8 +2994,9 @@ flowchart TD
|
|||||||
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
|
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
|
||||||
│ │
|
│ │
|
||||||
│ └── di/ # 全局 DI — 手动装配的 Provider
|
│ └── di/ # 全局 DI — 手动装配的 Provider
|
||||||
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + ApiClient + SocketConfig + SocketClient + SocketManager
|
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
|
||||||
│ └── app_providers.dart # 全局共享状态(themeModeProvider + AuthNotifier)
|
│ ├── db_provider.dart # StorageSdkApi(注入 AppDatabase factory)
|
||||||
|
│ └── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier
|
||||||
│
|
│
|
||||||
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
|
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
|
||||||
│ │
|
│ │
|
||||||
@@ -3022,6 +3030,7 @@ flowchart TD
|
|||||||
│ ├── di/
|
│ ├── di/
|
||||||
│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入)
|
│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入)
|
||||||
│ ├── presentation/
|
│ ├── presentation/
|
||||||
|
│ │ ├── settings_view_model.dart # @riverpod ViewModel(设置页导航)
|
||||||
│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart)
|
│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart)
|
||||||
│ ├── usecases/
|
│ ├── usecases/
|
||||||
│ │ └── set_theme_usecase.dart # 主题切换用例
|
│ │ └── set_theme_usecase.dart # 主题切换用例
|
||||||
@@ -3081,6 +3090,8 @@ flowchart TD
|
|||||||
│
|
│
|
||||||
└── ui/ # Core UI(设计系统 + 可复用组件)
|
└── ui/ # Core UI(设计系统 + 可复用组件)
|
||||||
├── base/ # 设计 Token
|
├── base/ # 设计 Token
|
||||||
|
│ ├── assets.dart # 静态资源路径常量(AppAssets:logo / 占位图)
|
||||||
|
│ ├── icons.dart # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态)
|
||||||
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
|
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
|
||||||
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
|
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
|
||||||
│ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors)
|
│ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors)
|
||||||
@@ -3172,7 +3183,7 @@ flowchart LR
|
|||||||
direction TB
|
direction TB
|
||||||
Step1["① 用户点击发送按钮"]
|
Step1["① 用户点击发送按钮"]
|
||||||
Step2["② ref.read(chatVM.notifier)<br/>.sendMessage(content)"]
|
Step2["② ref.read(chatVM.notifier)<br/>.sendMessage(content)"]
|
||||||
Step3["③ ViewModel 调用 UseCase<br/>→ Repository → ApiClient"]
|
Step3["③ ViewModel 调用 UseCase<br/>→ Repository → NetworksSdkApi"]
|
||||||
Step4["④ state = state.copyWith(<br/>messages: [..., newMsg])"]
|
Step4["④ state = state.copyWith(<br/>messages: [..., newMsg])"]
|
||||||
Step5["⑤ ref.watch(chatVM) 检测变化<br/>→ ConsumerWidget 自动 rebuild"]
|
Step5["⑤ ref.watch(chatVM) 检测变化<br/>→ ConsumerWidget 自动 rebuild"]
|
||||||
Step6["⑥ UI 展示最新消息列表"]
|
Step6["⑥ UI 展示最新消息列表"]
|
||||||
@@ -3356,7 +3367,7 @@ abstract class ChatRepository {
|
|||||||
|
|
||||||
// Data 层实现接口
|
// Data 层实现接口
|
||||||
class ChatRepositoryImpl implements ChatRepository {
|
class ChatRepositoryImpl implements ChatRepository {
|
||||||
final ApiClient _client;
|
final NetworksSdkApi _client;
|
||||||
final MessageLocalDataSource _localDataSource;
|
final MessageLocalDataSource _localDataSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -5623,7 +5634,7 @@ flowchart TD
|
|||||||
│ ├── file_storage.dart # 文件存储管理
|
│ ├── file_storage.dart # 文件存储管理
|
||||||
│ └── image_cache.dart # 图片缓存
|
│ └── image_cache.dart # 图片缓存
|
||||||
│
|
│
|
||||||
├── remote/ # Request 文件(一个端点一个文件,Repository 直接调 ApiClient)
|
├── remote/ # Request 文件(一个端点一个文件,Repository 直接调 NetworksSdkApi)
|
||||||
│ ├── login_request.dart # 登录端点
|
│ ├── login_request.dart # 登录端点
|
||||||
│ ├── logout_request.dart # 登出端点
|
│ ├── logout_request.dart # 登出端点
|
||||||
│ ├── send_message_request.dart # 发消息端点
|
│ ├── send_message_request.dart # 发消息端点
|
||||||
@@ -5648,17 +5659,17 @@ flowchart TD
|
|||||||
Domain[Domain Layer<br/>domain/repositories/<br/>Repository 接口] -.实现.-> Repo[Data Layer<br/>data/repositories/<br/>Repository 实现]
|
Domain[Domain Layer<br/>domain/repositories/<br/>Repository 接口] -.实现.-> Repo[Data Layer<br/>data/repositories/<br/>Repository 实现]
|
||||||
|
|
||||||
Repo -->|读取| LocalDS[Local DataSource<br/>data/local/]
|
Repo -->|读取| LocalDS[Local DataSource<br/>data/local/]
|
||||||
Repo -->|请求| ApiClient[ApiClient<br/>networks_sdk]
|
Repo -->|请求| SdkApi[NetworksSdkApi<br/>networks_sdk]
|
||||||
Repo -->|缓存| Cache[Cache Manager<br/>data/cache/]
|
Repo -->|缓存| Cache[Cache Manager<br/>data/cache/]
|
||||||
|
|
||||||
LocalDS -->|Drift| DB[(Database)]
|
LocalDS -->|Drift| DB[(Database)]
|
||||||
ApiClient -->|HTTP/WebSocket| API[API Server]
|
SdkApi -->|HTTP/WebSocket| API[API Server]
|
||||||
Cache -->|内存| Memory[Memory Cache]
|
Cache -->|内存| Memory[Memory Cache]
|
||||||
|
|
||||||
style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
|
style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
|
||||||
style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
|
style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
|
||||||
style LocalDS fill:#c8e6c9,stroke:#388e3c
|
style LocalDS fill:#c8e6c9,stroke:#388e3c
|
||||||
style ApiClient fill:#c8e6c9,stroke:#388e3c
|
style SdkApi fill:#c8e6c9,stroke:#388e3c
|
||||||
style Cache fill:#c8e6c9,stroke:#388e3c
|
style Cache fill:#c8e6c9,stroke:#388e3c
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -5686,14 +5697,14 @@ class MessageLocalDataSource {
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
||||||
<li><strong>Repository 直接调 ApiClient</strong>:无需 RemoteDataSource 中间层</li>
|
<li><strong>Repository 直接调 NetworksSdkApi</strong>:无需 RemoteDataSource 中间层</li>
|
||||||
<li><strong>@ApiRequest 注解 + 代码生成</strong>:自动实现 path / method / fromJson 注册</li>
|
<li><strong>@ApiRequest 注解 + 代码生成</strong>:自动实现 path / method / fromJson 注册</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<pre><code>// 示例:Repository 直接调用 Request
|
<pre><code>// 示例:Repository 直接调用 Request
|
||||||
// data/repositories/message_repository_impl.dart
|
// data/repositories/message_repository_impl.dart
|
||||||
class MessageRepositoryImpl implements MessageRepository {
|
class MessageRepositoryImpl implements MessageRepository {
|
||||||
final ApiClient _client;
|
final NetworksSdkApi _client;
|
||||||
|
|
||||||
Future<SendMessageData?> sendMessage({
|
Future<SendMessageData?> sendMessage({
|
||||||
required String chatId,
|
required String chatId,
|
||||||
@@ -5758,9 +5769,9 @@ flowchart LR
|
|||||||
|
|
||||||
RepoImpl -->|1. 检查缓存| Cache[Cache]
|
RepoImpl -->|1. 检查缓存| Cache[Cache]
|
||||||
RepoImpl -->|2. 读取本地| LocalDS[Local DS]
|
RepoImpl -->|2. 读取本地| LocalDS[Local DS]
|
||||||
RepoImpl -->|3. 请求远程| ApiClient2[ApiClient]
|
RepoImpl -->|3. 请求远程| SdkApi2[NetworksSdkApi]
|
||||||
|
|
||||||
ApiClient2 -->|DTO| RepoImpl
|
SdkApi2 -->|DTO| RepoImpl
|
||||||
RepoImpl -->|转换| Entity[Entity]
|
RepoImpl -->|转换| Entity[Entity]
|
||||||
Entity -->|返回| UC
|
Entity -->|返回| UC
|
||||||
|
|
||||||
@@ -5884,13 +5895,16 @@ flowchart LR
|
|||||||
├── data/
|
├── data/
|
||||||
│ ├── datasources/
|
│ ├── datasources/
|
||||||
│ │ ├── http/
|
│ │ ├── http/
|
||||||
│ │ │ ├── api_client.dart # Dio REST 客户端(executeRequest<T> 唯一入口)
|
│ │ │ ├── api_client.dart # Dio REST 客户端
|
||||||
|
│ │ │ ├── token_refresh_manager.dart # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用 + 主动刷新)
|
||||||
│ │ │ └── interceptor/
|
│ │ │ └── interceptor/
|
||||||
│ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
│ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
||||||
│ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
|
│ │ │ ├── encryption_interceptor.dart # 请求加密 / 响应解密(预留给 cipher_guard_sdk)
|
||||||
|
│ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试 + 业务错误钩子
|
||||||
│ │ │ └── logging_interceptor.dart # 请求/响应日志
|
│ │ │ └── logging_interceptor.dart # 请求/响应日志
|
||||||
│ │ └── socket/
|
│ │ ├── socket/
|
||||||
│ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream 输出)
|
│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream/Token 热更新/加密钩子)
|
||||||
|
│ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口(executeRequest / executeDownload)
|
||||||
│ ├── dto/
|
│ ├── dto/
|
||||||
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
|
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
|
||||||
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||||||
@@ -5899,22 +5913,23 @@ flowchart LR
|
|||||||
│ └── networks_messaging_repository_impl.dart
|
│ └── networks_messaging_repository_impl.dart
|
||||||
├── domain/
|
├── domain/
|
||||||
│ ├── entities/
|
│ ├── entities/
|
||||||
│ │ ├── api_error.dart # @freezed HTTP 错误联合类型
|
│ │ ├── api_error.dart # @freezed HTTP 错误联合类型(7 变体,含 cancelled)
|
||||||
|
│ │ ├── encrypted_request.dart # 加密请求结果数据类(path / headers / body 覆盖)
|
||||||
│ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
│ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
||||||
│ │ ├── socket_connection_state.dart # 连接状态 enum
|
│ │ ├── socket_connection_state.dart # 连接状态 enum
|
||||||
│ │ ├── http_method.dart # GET / POST / PUT / DELETE / PATCH
|
│ │ ├── http_method.dart # GET / POST / PUT / DELETE / PATCH
|
||||||
│ │ └── api_request_type.dart # request / login / upload
|
│ │ └── api_request_type.dart # request / login / upload / stream / download
|
||||||
│ └── repositories/
|
│ └── repositories/
|
||||||
│ ├── networks_sdk_repository.dart
|
│ ├── networks_sdk_repository.dart
|
||||||
│ └── networks_messaging_repository.dart
|
│ └── networks_messaging_repository.dart
|
||||||
└── presentation/
|
└── presentation/
|
||||||
├── facade/
|
├── facade/
|
||||||
│ ├── networks_sdk_api.dart # HTTP 公开 API 接口
|
│ ├── networks_sdk_api.dart # HTTP 公开 API 接口(含 executeDownload)
|
||||||
│ └── networks_messaging_api.dart # WebSocket 公开 API 接口
|
│ └── networks_messaging_api.dart # WebSocket 公开 API 接口(含 updateToken / sendBytes)
|
||||||
└── wiring/
|
└── wiring/
|
||||||
├── api_config.dart # HTTP 配置(baseURL / token / 回调)
|
├── api_config.dart # HTTP 配置(baseURL / token / 回调 / 加密 / 重试 / 主动刷新)
|
||||||
├── socket_config.dart # WebSocket 配置(心跳 / 重连策略)
|
├── socket_config.dart # WebSocket 配置(心跳 / 重连 / 加密钩子 / 压缩)
|
||||||
├── network_callbacks.dart # 回调类型定义(OnTokenRefresh 等)
|
├── network_callbacks.dart # 回调类型定义(认证 / 加密 / 业务错误 / 下载进度 / WS 加密)
|
||||||
├── networks_sdk_core.dart
|
├── networks_sdk_core.dart
|
||||||
├── networks_sdk_api_impl.dart
|
├── networks_sdk_api_impl.dart
|
||||||
├── networks_messaging_api_impl.dart
|
├── networks_messaging_api_impl.dart
|
||||||
@@ -5928,7 +5943,7 @@ flowchart LR
|
|||||||
<tr><th>职责</th><th>SDK (networks_sdk)</th><th>App 层 (im_app)</th></tr>
|
<tr><th>职责</th><th>SDK (networks_sdk)</th><th>App 层 (im_app)</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Dio 管理</td><td>ApiClient 内部创建管理</td><td>构造 ApiClient 传入 config</td></tr>
|
<tr><td>Dio 管理</td><td>ApiClient 内部创建管理</td><td>通过 NetworksSdkWiring.build(config:) 创建</td></tr>
|
||||||
<tr><td>baseURL</td><td>ApiConfig.baseURL</td><td>AppConfig.apiBaseUrl 提供初始值</td></tr>
|
<tr><td>baseURL</td><td>ApiConfig.baseURL</td><td>AppConfig.apiBaseUrl 提供初始值</td></tr>
|
||||||
<tr><td>Token 存储</td><td>ApiConfig.token(内存)</td><td>安全存储、持久化</td></tr>
|
<tr><td>Token 存储</td><td>ApiConfig.token(内存)</td><td>安全存储、持久化</td></tr>
|
||||||
<tr><td>Token 刷新</td><td>检测过期 → 调 onTokenRefresh</td><td>提供回调实现</td></tr>
|
<tr><td>Token 刷新</td><td>检测过期 → 调 onTokenRefresh</td><td>提供回调实现</td></tr>
|
||||||
@@ -5941,7 +5956,7 @@ flowchart LR
|
|||||||
<tr><td>WebSocket 重连</td><td>指数退避自动重连(1s→2s→4s→8s→16s→30s)</td><td>无需关心</td></tr>
|
<tr><td>WebSocket 重连</td><td>指数退避自动重连(1s→2s→4s→8s→16s→30s)</td><td>无需关心</td></tr>
|
||||||
<tr><td>WebSocket 生命周期</td><td>提供 onEnterForeground/Background</td><td>App 层调用(AppLifecycleListener)</td></tr>
|
<tr><td>WebSocket 生命周期</td><td>提供 onEnterForeground/Background</td><td>App 层调用(AppLifecycleListener)</td></tr>
|
||||||
<tr><td>WebSocket 消息解析</td><td>JSON.decode → Stream 输出</td><td>App 层按 type 过滤 + DTO 解析</td></tr>
|
<tr><td>WebSocket 消息解析</td><td>JSON.decode → Stream 输出</td><td>App 层按 type 过滤 + DTO 解析</td></tr>
|
||||||
<tr><td>Riverpod</td><td>无依赖</td><td>Provider 包装 ApiClient / SocketClient</td></tr>
|
<tr><td>Riverpod</td><td>无依赖</td><td>Provider 包装 NetworksSdkApi / SocketClient</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -5968,7 +5983,7 @@ flowchart LR
|
|||||||
<li><strong>一个端点 = 一个 Request 文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
<li><strong>一个端点 = 一个 Request 文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
||||||
<li><strong>Response DTO 必须有 <code>toEntity()</code></strong>:统一 DTO → Domain Entity 的转换入口</li>
|
<li><strong>Response DTO 必须有 <code>toEntity()</code></strong>:统一 DTO → Domain Entity 的转换入口</li>
|
||||||
<li><strong>持久化 DTO 和 Response DTO 分开</strong>:Response DTO(<code>XxxData</code>)在 request 文件中,持久化 DTO(<code>XxxDto</code>)在 <code>data/models/</code></li>
|
<li><strong>持久化 DTO 和 Response DTO 分开</strong>:Response DTO(<code>XxxData</code>)在 request 文件中,持久化 DTO(<code>XxxDto</code>)在 <code>data/models/</code></li>
|
||||||
<li><strong>禁止跳层</strong>:ViewModel → Repository(→ UseCase 按需)→ ApiClient,每层职责明确</li>
|
<li><strong>禁止跳层</strong>:ViewModel → Repository(→ UseCase 按需)→ NetworksSdkApi,每层职责明确</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h5>傻瓜式教程:从零开始定义并发送一个接口</h5>
|
<h5>傻瓜式教程:从零开始定义并发送一个接口</h5>
|
||||||
@@ -6112,10 +6127,10 @@ class LoginRequest extends ApiRequestable<LoginData> // ← 固定写法
|
|||||||
|
|
||||||
<!-- ────────── 第 2 步 ────────── -->
|
<!-- ────────── 第 2 步 ────────── -->
|
||||||
|
|
||||||
<h6>第 2 步:在 Repository 中调用 ApiClient,转为 Domain Entity</h6>
|
<h6>第 2 步:在 Repository 中调用 NetworksSdkApi,转为 Domain Entity</h6>
|
||||||
|
|
||||||
<p><strong>在哪写</strong>:<code>lib/data/repositories/auth_repository_impl.dart</code></p>
|
<p><strong>在哪写</strong>:<code>lib/data/repositories/auth_repository_impl.dart</code></p>
|
||||||
<p><strong>做什么</strong>:直接调 ApiClient.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。</p>
|
<p><strong>做什么</strong>:调 NetworksSdkApi.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。</p>
|
||||||
|
|
||||||
<pre><code class="language-dart">import 'package:networks_sdk/networks_sdk.dart';
|
<pre><code class="language-dart">import 'package:networks_sdk/networks_sdk.dart';
|
||||||
import '../../domain/entities/user.dart';
|
import '../../domain/entities/user.dart';
|
||||||
@@ -6123,11 +6138,11 @@ import '../../domain/repositories/auth_repository.dart';
|
|||||||
import '../remote/login_request.dart';
|
import '../remote/login_request.dart';
|
||||||
|
|
||||||
class AuthRepositoryImpl implements AuthRepository {
|
class AuthRepositoryImpl implements AuthRepository {
|
||||||
final ApiClient _client; // ← 直接注入 ApiClient
|
final NetworksSdkApi _client; // ← 注入 Facade 接口
|
||||||
final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
|
final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
|
||||||
|
|
||||||
AuthRepositoryImpl({
|
AuthRepositoryImpl({
|
||||||
required ApiClient client,
|
required NetworksSdkApi client,
|
||||||
required void Function(String?) onTokenUpdate,
|
required void Function(String?) onTokenUpdate,
|
||||||
}) : _client = client,
|
}) : _client = client,
|
||||||
_onTokenUpdate = onTokenUpdate;
|
_onTokenUpdate = onTokenUpdate;
|
||||||
@@ -6137,7 +6152,7 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
required String email,
|
required String email,
|
||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
// 1. 直接调 ApiClient,构造请求 → 发 HTTP → 自动解码 → 返回 DTO
|
// 1. 调 NetworksSdkApi,构造请求 → 发 HTTP → 自动解码 → 返回 DTO
|
||||||
final LoginData? loginData = await _client.executeRequest(
|
final LoginData? loginData = await _client.executeRequest(
|
||||||
LoginRequest(email: email, password: password),
|
LoginRequest(email: email, password: password),
|
||||||
);
|
);
|
||||||
@@ -6162,15 +6177,15 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
<p><strong>3.1 注册 Provider(DI 装配)</strong></p>
|
<p><strong>3.1 注册 Provider(DI 装配)</strong></p>
|
||||||
|
|
||||||
<p><strong>在哪写</strong>:<code>lib/features/{模块}/di/{模块}_providers.dart</code></p>
|
<p><strong>在哪写</strong>:<code>lib/features/{模块}/di/{模块}_providers.dart</code></p>
|
||||||
<p><strong>做什么</strong>:在 Feature 目录下创建 Provider 文件,注册该模块的 DI 链路(Repository → UseCase 按需)。<code>app/di/</code> 只提供 SDK 基础设施(ApiConfig / ApiClient),业务模块的 Provider 内聚在 Feature 目录下。</p>
|
<p><strong>做什么</strong>:在 Feature 目录下创建 Provider 文件,注册该模块的 DI 链路(Repository → UseCase 按需)。<code>app/di/</code> 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 Provider 内聚在 Feature 目录下。</p>
|
||||||
|
|
||||||
<pre><code class="language-dart">// ── features/auth/di/auth_providers.dart ──
|
<pre><code class="language-dart">// ── features/auth/di/auth_providers.dart ──
|
||||||
|
|
||||||
// Repository(直接注入 ApiClient + 回调组合多个 SDK 能力)
|
// Repository(注入 Facade 接口 + 回调组合多个 SDK 能力)
|
||||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||||
final apiConfig = ref.read(apiConfigProvider);
|
final apiConfig = ref.read(apiConfigProvider);
|
||||||
return AuthRepositoryImpl(
|
return AuthRepositoryImpl(
|
||||||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
onTokenUpdate: (token) {
|
onTokenUpdate: (token) {
|
||||||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||||||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||||||
@@ -6199,7 +6214,7 @@ import '../../../domain/repositories/message_repository.dart';
|
|||||||
// ── Repository ──
|
// ── Repository ──
|
||||||
final messageRepositoryProvider = Provider<MessageRepository>((ref) {
|
final messageRepositoryProvider = Provider<MessageRepository>((ref) {
|
||||||
return MessageRepositoryImpl(
|
return MessageRepositoryImpl(
|
||||||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6207,7 +6222,7 @@ final messageRepositoryProvider = Provider<MessageRepository>((ref) {
|
|||||||
// 如需 UseCase(多步编排、跨模块协调),参考 auth_providers.dart 中的 loginUseCaseProvider。
|
// 如需 UseCase(多步编排、跨模块协调),参考 auth_providers.dart 中的 loginUseCaseProvider。
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<p style="margin-bottom: 0;"><strong>原则</strong>:<code>app/di/</code> 只放 SDK 基础设施(ApiConfig / ApiClient),业务模块的 DI 链路(Repository → UseCase 按需)内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中。</p>
|
<p style="margin-bottom: 0;"><strong>原则</strong>:<code>app/di/</code> 只放 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路(Repository → UseCase 按需)内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><strong>3.2 编写 ViewModel</strong></p>
|
<p><strong>3.2 编写 ViewModel</strong></p>
|
||||||
@@ -6280,7 +6295,7 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
→ View: vm.doSomething(...)
|
→ View: vm.doSomething(...)
|
||||||
→ ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
|
→ ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
|
||||||
→ RepositoryImpl.doSomething() // data/repositories/
|
→ RepositoryImpl.doSomething() // data/repositories/
|
||||||
→ _client.executeRequest(XxxRequest) // 直接调 ApiClient
|
→ _client.executeRequest(XxxRequest) // 调 NetworksSdkApi
|
||||||
→ 自动注入 header → HTTP 请求 → 自动解码 → DTO
|
→ 自动注入 header → HTTP 请求 → 自动解码 → DTO
|
||||||
→ dto.toEntity() → Domain Entity
|
→ dto.toEntity() → Domain Entity
|
||||||
← state = state.copyWith(...) // 更新状态
|
← state = state.copyWith(...) // 更新状态
|
||||||
@@ -6295,8 +6310,8 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
→ LoginUseCase: 格式校验(邮箱 + 密码) // features/auth/usecases/
|
→ LoginUseCase: 格式校验(邮箱 + 密码) // features/auth/usecases/
|
||||||
→ LoginUseCase: authRepository.login(...)
|
→ LoginUseCase: authRepository.login(...)
|
||||||
→ AuthRepositoryImpl.login() // data/repositories/
|
→ AuthRepositoryImpl.login() // data/repositories/
|
||||||
→ _client.executeRequest(LoginRequest) // 直接调 ApiClient
|
→ _client.executeRequest(LoginRequest) // 调 NetworksSdkApi
|
||||||
→ AuthInterceptor → Dio.request → RetryInterceptor // 自动处理
|
→ Auth → Encryption → Dio.request → Retry → Logging // 拦截器链自动处理
|
||||||
← request.decodeResponse → LoginData.fromJson // 自动解码
|
← request.decodeResponse → LoginData.fromJson // 自动解码
|
||||||
← LoginData(DTO)
|
← LoginData(DTO)
|
||||||
→ onTokenUpdate(token) // 回调:内存写入 + 持久化
|
→ onTokenUpdate(token) // 回调:内存写入 + 持久化
|
||||||
@@ -6360,7 +6375,7 @@ class SendMessageRequest extends ApiRequestable<SendMessageData>
|
|||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<p>保存 → 自动生成 → 然后在 Repository 中直接调 ApiClient 就完了:</p>
|
<p>保存 → 自动生成 → 然后在 Repository 中调 NetworksSdkApi 就完了:</p>
|
||||||
|
|
||||||
<pre><code class="language-dart">// 在 MessageRepositoryImpl 中添加
|
<pre><code class="language-dart">// 在 MessageRepositoryImpl 中添加
|
||||||
Future<SendMessageData?> sendMessage({
|
Future<SendMessageData?> sendMessage({
|
||||||
@@ -6568,18 +6583,18 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// API 客户端 Provider(全局单例)
|
/// Networks SDK API Provider(全局单例,Facade 接口)
|
||||||
/// 内部自动挂载 AuthInterceptor / RetryInterceptor / LoggingInterceptor
|
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
|
||||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||||||
final config = ref.read(apiConfigProvider);
|
final config = ref.read(apiConfigProvider);
|
||||||
return ApiClient(config: config);
|
return NetworksSdkWiring.build(config: config);
|
||||||
});
|
});
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<h5>DI 装配总览</h5>
|
<h5>DI 装配总览</h5>
|
||||||
|
|
||||||
<pre><code>app/di/ ← 手动装配:SDK 基础设施
|
<pre><code>app/di/ ← 手动装配:SDK 基础设施
|
||||||
└── network_provider.dart → apiConfigProvider + apiClientProvider
|
└── network_provider.dart → apiConfigProvider + networkSdkApiProvider
|
||||||
|
|
||||||
features/{模块}/di/ ← 手动装配:业务模块 DI 链路(Repository → UseCase 按需)
|
features/{模块}/di/ ← 手动装配:业务模块 DI 链路(Repository → UseCase 按需)
|
||||||
├── auth/di/auth_providers.dart → authRepositoryProvider
|
├── auth/di/auth_providers.dart → authRepositoryProvider
|
||||||
@@ -6593,7 +6608,7 @@ features/{模块}/presentation/ ← @riverpod 自动生成:ViewModel
|
|||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<p><strong>di/ 目录的定位</strong>:只放<strong>需要手动装配的 Provider</strong>(构造注入、回调组合等)。ViewModel Provider 由 <code>@riverpod</code> 注解自动生成(写在 <code>presentation/</code> 下),不在 <code>di/</code> 中。</p>
|
<p><strong>di/ 目录的定位</strong>:只放<strong>需要手动装配的 Provider</strong>(构造注入、回调组合等)。ViewModel Provider 由 <code>@riverpod</code> 注解自动生成(写在 <code>presentation/</code> 下),不在 <code>di/</code> 中。</p>
|
||||||
<p><strong>最小化原则</strong>:<code>app/di/</code> 只提供 SDK 能力(ApiConfig / ApiClient),不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中,需要时才创建。</p>
|
<p><strong>最小化原则</strong>:<code>app/di/</code> 只提供 SDK 能力(ApiConfig / NetworksSdkApi),不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中,需要时才创建。</p>
|
||||||
|
|
||||||
<h5>SDK 间解耦:回调注入模式</h5>
|
<h5>SDK 间解耦:回调注入模式</h5>
|
||||||
|
|
||||||
@@ -6605,7 +6620,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
// final secureStorage = ref.read(secureStorageProvider); // storage_sdk(待接入)
|
// final secureStorage = ref.read(secureStorageProvider); // storage_sdk(待接入)
|
||||||
|
|
||||||
return AuthRepositoryImpl(
|
return AuthRepositoryImpl(
|
||||||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
onTokenUpdate: (token) {
|
onTokenUpdate: (token) {
|
||||||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||||||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||||||
@@ -6635,6 +6650,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
networkError: (msg) => showToast('网络错误: $msg'),
|
networkError: (msg) => showToast('网络错误: $msg'),
|
||||||
decodingError: (msg) => showToast('数据解析失败'),
|
decodingError: (msg) => showToast('数据解析失败'),
|
||||||
apiError: (code, msg) => showToast('服务端错误[$code]: $msg'),
|
apiError: (code, msg) => showToast('服务端错误[$code]: $msg'),
|
||||||
|
cancelled: () => {}, // 用户主动取消,通常不提示
|
||||||
unknown: (msg) => showToast('未知错误'),
|
unknown: (msg) => showToast('未知错误'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7137,9 +7153,9 @@ abstract class ProfileRepository {
|
|||||||
|
|
||||||
<pre><code>// data/repositories/profile_repository_impl.dart
|
<pre><code>// data/repositories/profile_repository_impl.dart
|
||||||
class ProfileRepositoryImpl implements ProfileRepository {
|
class ProfileRepositoryImpl implements ProfileRepository {
|
||||||
final ApiClient _client;
|
final NetworksSdkApi _client;
|
||||||
|
|
||||||
ProfileRepositoryImpl({required ApiClient client})
|
ProfileRepositoryImpl({required NetworksSdkApi client})
|
||||||
: _client = client;
|
: _client = client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -7166,7 +7182,7 @@ import '../../../app/di/network_provider.dart';
|
|||||||
// ── Repository ──
|
// ── Repository ──
|
||||||
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
|
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
|
||||||
return ProfileRepositoryImpl(
|
return ProfileRepositoryImpl(
|
||||||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7174,7 +7190,7 @@ final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
|
|||||||
// ViewModel 通过 @riverpod 注解自动生成 Provider,无需额外注册。
|
// ViewModel 通过 @riverpod 注解自动生成 Provider,无需额外注册。
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<p><strong>说明</strong>:Profile 属于简单模板,ViewModel 直接调 Repository,无需 UseCase 中间层。<code>app/di/</code> 只提供 SDK 基础设施(ApiConfig / ApiClient),业务模块的 DI 链路内聚在 Feature 目录下。</p>
|
<p><strong>说明</strong>:Profile 属于简单模板,ViewModel 直接调 Repository,无需 UseCase 中间层。<code>app/di/</code> 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路内聚在 Feature 目录下。</p>
|
||||||
|
|
||||||
<h4>Feature 结构图示</h4>
|
<h4>Feature 结构图示</h4>
|
||||||
|
|
||||||
@@ -7714,7 +7730,7 @@ sequenceDiagram
|
|||||||
participant Repo as domain/repositories/<br/>message_repository.dart
|
participant Repo as domain/repositories/<br/>message_repository.dart
|
||||||
participant RepoImpl as data/repositories/<br/>message_repository_impl.dart
|
participant RepoImpl as data/repositories/<br/>message_repository_impl.dart
|
||||||
participant LocalDS as data/local/<br/>message_local_ds.dart
|
participant LocalDS as data/local/<br/>message_local_ds.dart
|
||||||
participant SDK as networks_sdk/<br/>ApiClient / SocketClient
|
participant SDK as networks_sdk/<br/>NetworksSdkApi / SocketClient
|
||||||
participant WS as WebSocket Server
|
participant WS as WebSocket Server
|
||||||
|
|
||||||
UI->>VM: 1. 用户点击发送按钮
|
UI->>VM: 1. 用户点击发送按钮
|
||||||
@@ -7746,7 +7762,7 @@ sequenceDiagram
|
|||||||
<li><strong>Repository 接口</strong>:UseCase 通过 <code>domain/repositories/message_repository.dart</code> 接口调用</li>
|
<li><strong>Repository 接口</strong>:UseCase 通过 <code>domain/repositories/message_repository.dart</code> 接口调用</li>
|
||||||
<li><strong>Repository 实现</strong>:<code>data/repositories/message_repository_impl.dart</code> 实现具体逻辑</li>
|
<li><strong>Repository 实现</strong>:<code>data/repositories/message_repository_impl.dart</code> 实现具体逻辑</li>
|
||||||
<li><strong>本地优先</strong>:先保存到 <code>data/local/message_local_ds.dart</code></li>
|
<li><strong>本地优先</strong>:先保存到 <code>data/local/message_local_ds.dart</code></li>
|
||||||
<li><strong>网络发送</strong>:Repository 直接调 SDK(ApiClient / SocketClient)发送</li>
|
<li><strong>网络发送</strong>:Repository 调 SDK(NetworksSdkApi / SocketClient)发送</li>
|
||||||
<li><strong>服务器确认</strong>:WebSocket 服务器确认接收</li>
|
<li><strong>服务器确认</strong>:WebSocket 服务器确认接收</li>
|
||||||
<li><strong>状态更新</strong>:更新本地数据库中的消息状态</li>
|
<li><strong>状态更新</strong>:更新本地数据库中的消息状态</li>
|
||||||
<li><strong>数据返回</strong>:层层返回,最终更新 UI</li>
|
<li><strong>数据返回</strong>:层层返回,最终更新 UI</li>
|
||||||
@@ -7830,7 +7846,7 @@ abstract class MessageRepository {
|
|||||||
<pre><code>// data/repositories/message_repository_impl.dart
|
<pre><code>// data/repositories/message_repository_impl.dart
|
||||||
class MessageRepositoryImpl implements MessageRepository {
|
class MessageRepositoryImpl implements MessageRepository {
|
||||||
final MessageLocalDataSource _localDS;
|
final MessageLocalDataSource _localDS;
|
||||||
final ApiClient _client; // 直接注入 ApiClient / SocketClient
|
final NetworksSdkApi _client; // 注入 Facade 接口
|
||||||
|
|
||||||
MessageRepositoryImpl(this._localDS, this._client);
|
MessageRepositoryImpl(this._localDS, this._client);
|
||||||
|
|
||||||
@@ -7861,7 +7877,7 @@ class MessageRepositoryImpl implements MessageRepository {
|
|||||||
MessageRepository messageRepository(MessageRepositoryRef ref) {
|
MessageRepository messageRepository(MessageRepositoryRef ref) {
|
||||||
return MessageRepositoryImpl(
|
return MessageRepositoryImpl(
|
||||||
ref.watch(messageLocalDataSourceProvider),
|
ref.watch(messageLocalDataSourceProvider),
|
||||||
ref.watch(apiClientProvider), // 直接注入 ApiClient
|
ref.watch(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
@@ -7895,7 +7911,7 @@ sequenceDiagram
|
|||||||
participant RepoImpl as data/repositories/<br/>chat_repository_impl.dart
|
participant RepoImpl as data/repositories/<br/>chat_repository_impl.dart
|
||||||
participant Cache as data/cache/<br/>cache_manager.dart
|
participant Cache as data/cache/<br/>cache_manager.dart
|
||||||
participant LocalDS as data/local/<br/>chat_local_ds.dart
|
participant LocalDS as data/local/<br/>chat_local_ds.dart
|
||||||
participant SDK as networks_sdk/<br/>ApiClient
|
participant SDK as networks_sdk/<br/>NetworksSdkApi
|
||||||
|
|
||||||
UI->>VM: 1. 页面初始化
|
UI->>VM: 1. 页面初始化
|
||||||
VM->>UC: 2. 调用 LoadChatListUseCase
|
VM->>UC: 2. 调用 LoadChatListUseCase
|
||||||
@@ -7907,7 +7923,7 @@ sequenceDiagram
|
|||||||
else 缓存未命中
|
else 缓存未命中
|
||||||
RepoImpl->>LocalDS: 6b. 读取本地数据库
|
RepoImpl->>LocalDS: 6b. 读取本地数据库
|
||||||
LocalDS-->>RepoImpl: 7. 返回本地数据
|
LocalDS-->>RepoImpl: 7. 返回本地数据
|
||||||
RepoImpl->>SDK: 8. 直接调 ApiClient 请求远程数据
|
RepoImpl->>SDK: 8. 调 NetworksSdkApi 请求远程数据
|
||||||
SDK-->>RepoImpl: 9. 返回最新数据
|
SDK-->>RepoImpl: 9. 返回最新数据
|
||||||
RepoImpl->>LocalDS: 10. 更新本地数据库
|
RepoImpl->>LocalDS: 10. 更新本地数据库
|
||||||
RepoImpl->>Cache: 11. 更新缓存
|
RepoImpl->>Cache: 11. 更新缓存
|
||||||
@@ -8583,9 +8599,9 @@ abstract class ChatRepository {
|
|||||||
Future<void> sendMessage(Message message);
|
Future<void> sendMessage(Message message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Repository 实现层(直接注入 ApiClient)
|
// 2. Repository 实现层(注入 Facade 接口)
|
||||||
class ChatRepositoryImpl implements ChatRepository {
|
class ChatRepositoryImpl implements ChatRepository {
|
||||||
final ApiClient client;
|
final NetworksSdkApi client;
|
||||||
final LocalDataSource localDataSource;
|
final LocalDataSource localDataSource;
|
||||||
final MessageMapper mapper;
|
final MessageMapper mapper;
|
||||||
|
|
||||||
@@ -8603,7 +8619,7 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
return localDTOs.map(mapper.toEntity).toList();
|
return localDTOs.map(mapper.toEntity).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接调 ApiClient 从远程获取
|
// 调 NetworksSdkApi 从远程获取
|
||||||
final response = await client.executeRequest(
|
final response = await client.executeRequest(
|
||||||
GetMessagesRequest(chatId: chatId),
|
GetMessagesRequest(chatId: chatId),
|
||||||
);
|
);
|
||||||
@@ -9770,6 +9786,146 @@ flowchart TD
|
|||||||
<p><strong>单一职责</strong>:每个模块只做一件事,UseCase/ViewModel/Repository 各司其职</p>
|
<p><strong>单一职责</strong>:每个模块只做一件事,UseCase/ViewModel/Repository 各司其职</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════ 第八部分:UI 设计规范 ═══════════════════════ -->
|
||||||
|
|
||||||
|
<h2 id="part8-ui-design" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第八部分:UI 设计规范</h2>
|
||||||
|
|
||||||
|
<p>本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。</p>
|
||||||
|
|
||||||
|
<h3 id="8-0-核心约定">8.0 核心约定</h3>
|
||||||
|
|
||||||
|
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #1565c0; margin: 20px 0;">
|
||||||
|
<p style="margin-top: 0; font-weight: 700; color: #1565c0;">全局只有一份</p>
|
||||||
|
<p>颜色、字体、基础组件、业务弹框、图片、图标——Figma 里每种元素只有一个定义。没有"备用版本",没有"临时副本",不允许两个"差不多一样"的组件并存。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li><strong>Figma 命名是重中之重</strong>:点中任何元素都必须看到抽象名称。六类无例外:
|
||||||
|
<ul>
|
||||||
|
<li><code>颜色</code> — 如 <code>primary</code>、<code>surface</code></li>
|
||||||
|
<li><code>字体</code> — 如 <code>Body/Medium</code>、<code>Label/Small</code></li>
|
||||||
|
<li><code>基础组件</code> — 如 <code>Button/Primary</code>、<code>Input/Default</code>,全局只有一个版本</li>
|
||||||
|
<li><code>业务弹框</code> — 如 <code>Dialog/Confirm</code></li>
|
||||||
|
<li><code>图片</code> — Figma 统一导出,代码侧 <code>AppAssets</code> 注册,不硬编码路径</li>
|
||||||
|
<li><code>图标</code> — 如 <code>send</code>、<code>more_options</code>,代码侧 <code>AppIcons</code> 调用</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>基础组件定稿后不随意改动</strong>:需改动时必须先告知研发,评估影响范围,双方同步后再执行</li>
|
||||||
|
<li><strong>UI 团队自主维护 UI 基建体系</strong>:研发照着 Figma 名字封装,名称必须完全一致</li>
|
||||||
|
<li><strong>所有元素遵循同一套命名规则</strong>:新增任何元素先在 Figma 定名,研发用相同名字注册</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div style="background: #FFF8E6; border-left: 3px solid #F2C94C; border-radius: 0 8px 8px 0; padding: 12px 16px; margin: 16px 0; font-size: 14px; color: #5F4B00;">
|
||||||
|
<strong>图片和组件是重灾区:</strong>没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="8-1-颜色体系">8.1 颜色体系</h3>
|
||||||
|
|
||||||
|
<p>所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。</p>
|
||||||
|
|
||||||
|
<h4>语义色(随主题变化)</h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>抽象名</th><th>Figma 名</th><th>亮色</th><th>暗色</th><th>用途</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>primary</code></td><td>Primary</td><td>#2F80ED</td><td>#5BA3F5</td><td>主操作、链接、选中态</td></tr>
|
||||||
|
<tr><td><code>background</code></td><td>Background</td><td>#F8F9FA</td><td>#202124</td><td>页面底色</td></tr>
|
||||||
|
<tr><td><code>surface</code></td><td>Surface</td><td>#FFFFFF</td><td>#3C4043</td><td>卡片、弹框、输入框</td></tr>
|
||||||
|
<tr><td><code>onSurface</code></td><td>On Surface</td><td>#202124</td><td>#FFFFFF</td><td>surface 上的文字</td></tr>
|
||||||
|
<tr><td><code>error</code></td><td>Error</td><td colspan="2">#EB5757</td><td>错误状态</td></tr>
|
||||||
|
<tr><td><code>success</code></td><td>Success</td><td colspan="2">#27AE60</td><td>成功状态</td></tr>
|
||||||
|
<tr><td><code>warning</code></td><td>Warning</td><td colspan="2">#F2C94C</td><td>警告状态</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>灰阶(固定值,不随主题变化)</h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>名称</th><th>色值</th><th>名称</th><th>色值</th><th>名称</th><th>色值</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>white</td><td>#FFFFFF</td><td>gray50</td><td>#F8F9FA</td><td>gray100</td><td>#F1F3F4</td></tr>
|
||||||
|
<tr><td>gray200</td><td>#E8EAED</td><td>gray400</td><td>#BDC1C6</td><td>gray600</td><td>#80868B</td></tr>
|
||||||
|
<tr><td>gray800</td><td>#3C4043</td><td>gray900</td><td>#202124</td><td>black</td><td>#000000</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><strong>使用原则</strong>:需随主题切换 → 用语义色(<code>primary</code>、<code>surface</code>);亮暗保持不变 → 用灰阶固定值。</p>
|
||||||
|
|
||||||
|
<h3 id="8-2-字体体系">8.2 字体体系</h3>
|
||||||
|
|
||||||
|
<p>字体按层级分五档:Display、Headline、Title、Body、Label,每档三个尺寸。Figma 中按 <strong>层级/尺寸</strong> 格式命名(如 <code>Body/Large</code>),开发用同名变量调用。</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Figma 名称</th><th>字号</th><th>字重</th><th>行高</th><th>字距</th><th>典型用途</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">DISPLAY</td></tr>
|
||||||
|
<tr><td>Display/Large</td><td>57</td><td>400</td><td>64</td><td>-0.25</td><td>启动页大标题、空状态</td></tr>
|
||||||
|
<tr><td>Display/Medium</td><td>45</td><td>400</td><td>52</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td>Display/Small</td><td>36</td><td>400</td><td>44</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">HEADLINE</td></tr>
|
||||||
|
<tr><td>Headline/Large</td><td>32</td><td>400</td><td>40</td><td>—</td><td>页面主标题、导航栏</td></tr>
|
||||||
|
<tr><td>Headline/Medium</td><td>28</td><td>400</td><td>36</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td>Headline/Small</td><td>24</td><td>400</td><td>32</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">TITLE</td></tr>
|
||||||
|
<tr><td>Title/Large</td><td>22</td><td>500</td><td>28</td><td>—</td><td>会话列表名称、设置项标题</td></tr>
|
||||||
|
<tr><td>Title/Medium</td><td>16</td><td>500</td><td>24</td><td>0.15</td><td>卡片标题、列表主行</td></tr>
|
||||||
|
<tr><td>Title/Small</td><td>14</td><td>500</td><td>20</td><td>0.1</td><td>—</td></tr>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">BODY</td></tr>
|
||||||
|
<tr><td>Body/Large</td><td>16</td><td>400</td><td>24</td><td>0.5</td><td>聊天气泡、表单输入</td></tr>
|
||||||
|
<tr><td>Body/Medium</td><td>14</td><td>400</td><td>20</td><td>0.25</td><td>正文说明、列表副行</td></tr>
|
||||||
|
<tr><td>Body/Small</td><td>12</td><td>400</td><td>16</td><td>0.4</td><td>辅助信息、提示文字</td></tr>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">LABEL</td></tr>
|
||||||
|
<tr><td>Label/Large</td><td>14</td><td>500</td><td>20</td><td>0.1</td><td>按钮文字、Tab 标签、Badge</td></tr>
|
||||||
|
<tr><td>Label/Medium</td><td>12</td><td>500</td><td>16</td><td>0.5</td><td>次要标签、徽标文字</td></tr>
|
||||||
|
<tr><td>Label/Small</td><td>11</td><td>500</td><td>16</td><td>0.5</td><td>最小粒度标签</td></tr>
|
||||||
|
<tr><td colspan="6" style="font-weight:700;color:#80868B;font-size:12px;">语义样式</td></tr>
|
||||||
|
<tr><td>Section Label</td><td>13</td><td>600</td><td>—</td><td>0.5</td><td>列表分组标题、设置分区</td></tr>
|
||||||
|
<tr><td>Body/Muted</td><td>12</td><td>400</td><td>16</td><td>—</td><td>说明文字(灰色,低对比度)</td></tr>
|
||||||
|
<tr><td>Body/Error</td><td>12</td><td>400</td><td>16</td><td>—</td><td>表单错误提示(红色)</td></tr>
|
||||||
|
<tr><td>Label/Muted</td><td>12</td><td>500</td><td>16</td><td>—</td><td>时间戳、元数据(低对比度)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 id="8-3-组件-button">8.3 组件 — Button</h3>
|
||||||
|
|
||||||
|
<p>按钮共四种变体,每种有明确使用场景和 Figma 组件名。每个页面上主操作只用一个 Primary。</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Figma 组件名</th><th>用途</th><th>亮色样式</th><th>暗色样式</th><th>状态</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>Button/Primary</code></td><td>主操作(登录、发送、确认),每屏最多一次</td><td>背景 #2F80ED,白字</td><td>背景 #5BA3F5,白字</td><td>默认 / Loading / 禁用(#BDC1C6)</td></tr>
|
||||||
|
<tr><td><code>Button/Secondary</code></td><td>次要操作(注册、稍后再说),描边样式</td><td>描边 #2F80ED,蓝字</td><td>描边 #5BA3F5,蓝字</td><td>默认 / 禁用</td></tr>
|
||||||
|
<tr><td><code>Button/Text</code></td><td>辅助链接(忘记密码、查看全部、弹框取消)</td><td>无背景,蓝字</td><td>无背景,蓝字</td><td>默认</td></tr>
|
||||||
|
<tr><td><code>Button/Inverse</code></td><td>反色按钮(深色背景高亮),支持左侧图标</td><td>背景 #202124,白字</td><td>背景 #FFFFFF,黑字</td><td>默认</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 id="8-4-业务弹框-dialog">8.4 业务弹框 — Dialog</h3>
|
||||||
|
|
||||||
|
<p>当前封装一种通用确认弹框 <code>Dialog/Confirm</code>,后续新增先在 Figma 以 <code>Dialog/</code> 前缀命名。</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>项目</th><th>说明</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>结构</td><td>标题(不超 15 字) + 内容 + 操作区(取消 Text 样式 | 确认 Primary 样式)</td></tr>
|
||||||
|
<tr><td>可配置</td><td>标题文字、内容文字、确认/取消标签、点击背景是否可关闭</td></tr>
|
||||||
|
<tr><td>返回值</td><td>确认 / 取消 / 关闭(点击背景)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 id="8-5-图标规范">8.5 图标规范</h3>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li><strong>UI 先命名,开发跟随</strong>:Figma 确定名称,开发用完全相同的名称封装到 <code>AppIcons</code></li>
|
||||||
|
<li><strong>名称有实际语义</strong>:全小写下划线,如 <code>send</code>、<code>add_contact</code>、<code>more_options</code>。不用拼音,不缩写</li>
|
||||||
|
<li><strong>统一用 AppIcons 调用</strong>:不允许直接用裸 icon 库变量,替换时改一处全局生效</li>
|
||||||
|
<li><strong>同义图标只保留一个</strong>:同功能图标在整个产品内只存在一种</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p><strong>新增图标流程</strong>:设计师 Figma 确认名称 → 告知开发 → 开发用相同名称在 AppIcons 注册。两端名称必须完全一致。</p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ plugins {
|
|||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -38,5 +38,10 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
# 对没有在 podspec 里声明 swift_version 的 pod 设置兜底版本。
|
||||||
|
# 已有 swift_version 的 pod(含第三方如 Agora)CocoaPods 优先使用其 podspec 值,不受影响。
|
||||||
|
config.build_settings['SWIFT_VERSION'] ||= '6.2'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -164,7 +164,6 @@
|
|||||||
1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */,
|
1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */,
|
9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -489,7 +488,7 @@
|
|||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
@@ -508,7 +507,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -524,7 +523,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -540,7 +539,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
@@ -678,7 +677,7 @@
|
|||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
@@ -705,7 +704,7 @@
|
|||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
|
||||||
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
@@ -16,9 +16,11 @@ import UIKit
|
|||||||
sceneConfig.delegateClass = SceneDelegate.self
|
sceneConfig.delegateClass = SceneDelegate.self
|
||||||
return sceneConfig
|
return sceneConfig
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - FlutterImplicitEngineDelegate
|
// FlutterImplicitEngineDelegate 来自 Flutter ObjC 框架,尚未标注 @MainActor,
|
||||||
|
// 用 @preconcurrency 抑制 Swift 6 ConformanceIsolation 错误。
|
||||||
|
extension AppDelegate: @preconcurrency FlutterImplicitEngineDelegate {
|
||||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:networks_sdk/networks_sdk.dart';
|
|||||||
import '../../core/foundation/api_paths.dart';
|
import '../../core/foundation/api_paths.dart';
|
||||||
import '../../core/foundation/config.dart';
|
import '../../core/foundation/config.dart';
|
||||||
import '../../core/foundation/constants.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/network_monitor.dart';
|
||||||
import '../../core/services/socket_manager.dart';
|
import '../../core/services/socket_manager.dart';
|
||||||
|
|
||||||
@@ -47,6 +49,21 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
|||||||
return monitor;
|
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 基础设施 ─────────────────────────────────────────────────────────────
|
// ── HTTP 基础设施 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// API 配置 Provider(全局单例)
|
/// API 配置 Provider(全局单例)
|
||||||
@@ -58,15 +75,18 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
|||||||
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
|
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
|
||||||
final apiConfigProvider = Provider<ApiConfig>((ref) {
|
final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||||
final networkMonitor = ref.read(networkMonitorProvider);
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
final tokenStream = ref.read(_tokenUpdateStreamProvider);
|
||||||
|
|
||||||
return ApiConfig(
|
return ApiConfig(
|
||||||
baseURL: AppConfig.apiBaseUrl,
|
baseURL: AppConfig.apiBaseUrl,
|
||||||
platformHeaders: {
|
platformHeaders: {
|
||||||
'Platform': 'Android', // TODO: 运行时从平台 API 获取
|
'Platform': 'Android', // TODO: 运行时从 platform API 获取
|
||||||
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
||||||
|
'Channel': '', // TODO: 从 AppConfig 读取渠道标识
|
||||||
|
'lang': 'zh-CN', // TODO: 从 l10n_sdk 或系统 locale 动态获取
|
||||||
},
|
},
|
||||||
tokenExpiredCodes: {30002, 30003, 30124},
|
tokenExpiredCodes: ApiErrorCodes.tokenExpiredCodes,
|
||||||
forceLogoutCodes: {30125},
|
forceLogoutCodes: ApiErrorCodes.forceLogoutCodes,
|
||||||
onForceLogout: () {
|
onForceLogout: () {
|
||||||
// TODO: 清除登录态,跳转登录页
|
// TODO: 清除登录态,跳转登录页
|
||||||
},
|
},
|
||||||
@@ -74,7 +94,17 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
|||||||
// TODO: App 层刷新 token 逻辑
|
// TODO: App 层刷新 token 逻辑
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
onTokenUpdated: (newToken) {
|
||||||
|
// 通过事件流同步到 WebSocket,避免直接引用 socketManagerProvider 造成循环依赖
|
||||||
|
tokenStream.add(newToken);
|
||||||
|
},
|
||||||
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
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,
|
maxRetries: AppConstants.maxRetries,
|
||||||
retryBaseDelay: AppConstants.retryBaseDelay,
|
retryBaseDelay: AppConstants.retryBaseDelay,
|
||||||
onLog: (message, {tag}) {
|
onLog: (message, {tag}) {
|
||||||
@@ -94,16 +124,47 @@ final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
|||||||
|
|
||||||
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// SocketConfig Provider(全局单例)
|
/// SocketConfig Provider(内部使用,不对外暴露)
|
||||||
///
|
///
|
||||||
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
||||||
/// SDK 内部不调用其他 SDK。
|
/// SDK 内部不调用其他 SDK。
|
||||||
final socketConfigProvider = Provider<SocketConfig>((ref) {
|
final _socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||||
final networkMonitor = ref.read(networkMonitorProvider);
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
|
||||||
return SocketConfig(
|
return SocketConfig(
|
||||||
maxReconnectAttempts: AppConstants.maxRetries,
|
maxReconnectAttempts: AppConstants.maxRetries,
|
||||||
maxReconnectDelay: AppConstants.maxReconnectDelay,
|
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}) {
|
onLog: (message, {tag}) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[${tag ?? 'Socket'}] $message');
|
print('[${tag ?? 'Socket'}] $message');
|
||||||
@@ -114,12 +175,11 @@ final socketConfigProvider = Provider<SocketConfig>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// SocketClient Provider(全局单例)
|
/// SocketClient Provider(内部使用,不对外暴露)
|
||||||
///
|
///
|
||||||
/// 与 apiClientProvider 对称。
|
/// 与 networkSdkApiProvider 对称。
|
||||||
final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
final _socketClientProvider = Provider<NetworksMessagingApi>((ref) {
|
||||||
{
|
final config = ref.read(_socketConfigProvider);
|
||||||
final config = ref.read(socketConfigProvider);
|
|
||||||
return NetworksMessagingApi()..initialize(config);
|
return NetworksMessagingApi()..initialize(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,17 +199,43 @@ final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
|||||||
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
||||||
/// 自动触发断连/重连。
|
/// 自动触发断连/重连。
|
||||||
///
|
///
|
||||||
|
/// Token 更新由 [_tokenUpdateStreamProvider] 事件流驱动,
|
||||||
|
/// HTTP 层刷新 token 后自动同步到 WebSocket。
|
||||||
|
///
|
||||||
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
||||||
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
||||||
/// SDK 内部不调用其他 SDK。
|
/// SDK 内部不调用其他 SDK。
|
||||||
final socketManagerProvider = Provider<SocketManager>((ref) {
|
final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||||
final client = ref.read(socketClientProvider);
|
final client = ref.read(_socketClientProvider);
|
||||||
final networkMonitor = ref.read(networkMonitorProvider);
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
final apiConfig = ref.read(apiConfigProvider);
|
||||||
|
final tokenStream = ref.read(_tokenUpdateStreamProvider);
|
||||||
|
|
||||||
final manager = SocketManager(
|
final manager = SocketManager(
|
||||||
client: client,
|
client: client,
|
||||||
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
||||||
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
|
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,
|
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||||
onLog: (message, {tag}) {
|
onLog: (message, {tag}) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
@@ -157,13 +243,19 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 监听 token 更新事件 → 同步到 WebSocket
|
||||||
|
final tokenSub = tokenStream.stream.listen((newToken) {
|
||||||
|
manager.updateToken(newToken);
|
||||||
|
});
|
||||||
|
|
||||||
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
||||||
final subscription = networkMonitor.onStatusChanged.listen((isAvailable) {
|
final networkSub = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||||
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
subscription.cancel();
|
tokenSub.cancel();
|
||||||
|
networkSub.cancel();
|
||||||
unawaited(manager.dispose());
|
unawaited(manager.dispose());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,23 +307,55 @@ String _buildWsUrl(String httpBaseUrl) {
|
|||||||
// Provider 链路:
|
// Provider 链路:
|
||||||
//
|
//
|
||||||
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
||||||
// ├── apiConfigProvider → apiClientProvider ← HTTP 层
|
// ├── apiConfigProvider → networkSdkApiProvider ← HTTP 层
|
||||||
// └── socketConfigProvider → socketClientProvider ← WS 层
|
// └── _socketConfigProvider → _socketClientProvider ← WS 层(内部)
|
||||||
// → socketManagerProvider
|
// → socketManagerProvider
|
||||||
//
|
//
|
||||||
|
// _tokenUpdateStreamProvider(打破循环引用的中间层)
|
||||||
|
// ← apiConfigProvider.onTokenUpdated 推送
|
||||||
|
// → socketManagerProvider 监听 → socketManager.updateToken()
|
||||||
|
//
|
||||||
// 网络事件驱动链路:
|
// 网络事件驱动链路:
|
||||||
//
|
//
|
||||||
// connectivity_plus(平台网络事件)
|
// connectivity_plus(平台网络事件)
|
||||||
// → NetworkMonitor.onStatusChanged(true / false)
|
// → NetworkMonitor.onStatusChanged(true / false)
|
||||||
// → SocketManager.handleNetworkStatusChanged()
|
// → SocketManager.handleNetworkStatusChanged()
|
||||||
// → 断网: disconnect()
|
// → 断网: disconnect()
|
||||||
// → 恢复: connect(token: lastToken)
|
// → 恢复: onBeforeReconnect → connect(token: lastToken)
|
||||||
//
|
//
|
||||||
// 前后台事件驱动链路:
|
// 前后台事件驱动链路:
|
||||||
//
|
//
|
||||||
// WidgetsBindingObserver(App 层 app.dart)
|
// WidgetsBindingObserver(App 层 app.dart)
|
||||||
// → SocketManager.onEnterBackground() → disconnect
|
// → SocketManager.onEnterBackground()
|
||||||
// → SocketManager.onEnterForeground() → reconnect
|
// 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 能力:
|
// Repository 直接注入 ApiClient,通过回调注入其他 SDK 能力:
|
||||||
//
|
//
|
||||||
@@ -313,7 +437,7 @@ String _buildWsUrl(String httpBaseUrl) {
|
|||||||
// final authRepositoryProvider = Provider((ref) {
|
// final authRepositoryProvider = Provider((ref) {
|
||||||
// final apiConfig = ref.read(apiConfigProvider);
|
// final apiConfig = ref.read(apiConfigProvider);
|
||||||
// return AuthRepositoryImpl(
|
// return AuthRepositoryImpl(
|
||||||
// client: ref.read(apiClientProvider), // 直接注入
|
// client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
// onTokenUpdate: (token) {
|
// onTokenUpdate: (token) {
|
||||||
// apiConfig.updateToken(token); // 内存(network_sdk)
|
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||||||
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/// API 错误码常量
|
||||||
|
///
|
||||||
|
/// 集中管理后端业务错误码,避免散落在各处硬编码。
|
||||||
|
/// 按业务域分组,命名风格对齐后端定义。
|
||||||
|
///
|
||||||
|
/// 使用方式:
|
||||||
|
/// ```dart
|
||||||
|
/// ApiConfig(
|
||||||
|
/// tokenExpiredCodes: ApiErrorCodes.tokenExpiredCodes,
|
||||||
|
/// forceLogoutCodes: ApiErrorCodes.forceLogoutCodes,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class ApiErrorCodes {
|
||||||
|
ApiErrorCodes._();
|
||||||
|
|
||||||
|
// ── 认证(30001-30009)──
|
||||||
|
|
||||||
|
/// Token 无效
|
||||||
|
static const int tokenInvalid = 30002;
|
||||||
|
|
||||||
|
/// JWT 无效
|
||||||
|
static const int jwtInvalid = 30003;
|
||||||
|
|
||||||
|
/// 签名方法错误
|
||||||
|
static const int signingMethodError = 30008;
|
||||||
|
|
||||||
|
/// 密钥解析失败
|
||||||
|
static const int parsingKeyError = 30009;
|
||||||
|
|
||||||
|
/// Session 无效
|
||||||
|
static const int sessionInvalid = 30124;
|
||||||
|
|
||||||
|
/// Refresh Token 失效
|
||||||
|
static const int refreshTokenFailed = 30125;
|
||||||
|
|
||||||
|
/// 账号在其他设备登录
|
||||||
|
static const int loggedInAnotherDevice = 30006;
|
||||||
|
|
||||||
|
// ── 错误码集合 ──
|
||||||
|
|
||||||
|
/// Token 过期错误码集合 — 触发自动刷新 Token
|
||||||
|
static const Set<int> tokenExpiredCodes = {
|
||||||
|
tokenInvalid,
|
||||||
|
jwtInvalid,
|
||||||
|
sessionInvalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 强制登出错误码集合 — 触发退出登录流程
|
||||||
|
static const Set<int> forceLogoutCodes = {refreshTokenFailed};
|
||||||
|
|
||||||
|
/// 踢下线错误码集合 — 触发踢下线 UI 提示
|
||||||
|
static const Set<int> kickOffCodes = {
|
||||||
|
loggedInAnotherDevice,
|
||||||
|
signingMethodError,
|
||||||
|
parsingKeyError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
|
|
||||||
|
/// JWT token 过期时间解析
|
||||||
|
///
|
||||||
|
/// 使用 dart_jsonwebtoken 解码 JWT payload,提取 `exp` claim 返回过期时间。
|
||||||
|
/// 返回 null 表示无法解析(非 JWT 格式或缺少 exp 字段)。
|
||||||
|
///
|
||||||
|
/// 只读取 payload,不验证签名(验证是服务端的事)。
|
||||||
|
///
|
||||||
|
/// 用于 [ApiConfig.onGetTokenExpiry] 回调,启用 token 主动刷新:
|
||||||
|
/// 距过期不足阈值时提前刷新,避免带过期 token 发请求或重连。
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final expiry = parseJwtExpiry('eyJhbGci...');
|
||||||
|
/// if (expiry != null) {
|
||||||
|
/// final remaining = expiry.difference(DateTime.now());
|
||||||
|
/// print('Token expires in ${remaining.inMinutes} min');
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
DateTime? parseJwtExpiry(String token) {
|
||||||
|
try {
|
||||||
|
final jwt = JWT.decode(token);
|
||||||
|
final payload = jwt.payload;
|
||||||
|
if (payload is! Map<String, dynamic>) return null;
|
||||||
|
|
||||||
|
final exp = payload['exp'];
|
||||||
|
if (exp is! int) return null;
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
|
||||||
import 'package:networks_sdk/networks_sdk.dart';
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
import 'network_backoff_debouncer.dart';
|
import 'network_backoff_debouncer.dart';
|
||||||
@@ -10,9 +9,8 @@ import 'network_backoff_debouncer.dart';
|
|||||||
/// 参考 HTTP 层 onTokenRefresh 的回调注入模式。
|
/// 参考 HTTP 层 onTokenRefresh 的回调注入模式。
|
||||||
/// App 层在 Provider 装配时注入解密/解析逻辑,
|
/// App 层在 Provider 装配时注入解密/解析逻辑,
|
||||||
/// 不在 SDK 内部调用加解密 SDK。
|
/// 不在 SDK 内部调用加解密 SDK。
|
||||||
typedef MessageTransformer = Map<String, dynamic> Function(
|
typedef MessageTransformer =
|
||||||
Map<String, dynamic> raw,
|
Map<String, dynamic> Function(Map<String, dynamic> raw);
|
||||||
);
|
|
||||||
|
|
||||||
/// WebSocket 连接管理
|
/// WebSocket 连接管理
|
||||||
///
|
///
|
||||||
@@ -39,19 +37,26 @@ typedef MessageTransformer = Map<String, dynamic> Function(
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
||||||
/// App 进后台 → onEnterBackground() → 断开连接(省电)
|
///
|
||||||
/// App 回前台 → onEnterForeground() → 检查网络 → 自动重连
|
/// ── disconnectInBackground = true(默认,移动端)──
|
||||||
|
/// App 进后台 → onEnterBackground() → 暂停心跳 + 断开连接(省电)
|
||||||
|
/// App 回前台 → onEnterForeground() → 恢复心跳 → onBeforeReconnect → 重连
|
||||||
|
///
|
||||||
|
/// ── disconnectInBackground = false(桌面端)──
|
||||||
|
/// App 进后台 → onEnterBackground() → 不操作,完全保活
|
||||||
|
/// App 回前台 → onEnterForeground() → 不操作(连接始终在线)
|
||||||
|
///
|
||||||
/// 网络丢失 → handleNetworkLost() → 断开连接
|
/// 网络丢失 → handleNetworkLost() → 断开连接
|
||||||
/// 网络恢复 → handleNetworkRestored() → 退避重连(防抖动)
|
/// 网络恢复 → handleNetworkRestored() → 退避 → onBeforeReconnect → 重连
|
||||||
/// 登出 → disconnect() → 断开连接,清除 token
|
/// 登出 → disconnect() → 断开连接,清除 token
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## 前置检查策略
|
/// ## 前置检查策略
|
||||||
///
|
///
|
||||||
/// 所有会发起网络操作的方法都先检查前置条件:
|
/// 所有会发起网络操作的方法都先检查前置条件:
|
||||||
/// - connect → 检查网络可用性 + 是否在后台
|
/// - connect → 检查网络可用性 + 是否在后台(仅 disconnectInBackground=true 时拦截)
|
||||||
/// - send / sendString → 检查连接状态 + 是否在后台
|
/// - send / sendString → 检查连接状态 + 是否在后台(仅 disconnectInBackground=true 时拦截)
|
||||||
/// - onEnterForeground 重连 → 检查网络可用性
|
/// - onEnterForeground / 网络恢复重连 → 检查网络可用性 + onBeforeReconnect
|
||||||
class SocketManager {
|
class SocketManager {
|
||||||
final NetworksMessagingApi _client;
|
final NetworksMessagingApi _client;
|
||||||
final String _wsUrl;
|
final String _wsUrl;
|
||||||
@@ -70,6 +75,22 @@ class SocketManager {
|
|||||||
/// 连接和重连前调用,无网络时跳过操作并标记恢复时重试。
|
/// 连接和重连前调用,无网络时跳过操作并标记恢复时重试。
|
||||||
final Future<bool> Function()? onCheckNetworkAvailable;
|
final Future<bool> Function()? onCheckNetworkAvailable;
|
||||||
|
|
||||||
|
/// 重连前回调
|
||||||
|
///
|
||||||
|
/// 在 WebSocket 重连前调用(前台恢复、网络恢复),App 层用于:
|
||||||
|
/// - 检查并刷新即将过期的 token
|
||||||
|
/// - 更新连接参数
|
||||||
|
///
|
||||||
|
/// 回调完成后才发起实际重连。
|
||||||
|
final Future<void> Function()? onBeforeReconnect;
|
||||||
|
|
||||||
|
/// 进后台时是否断开连接
|
||||||
|
///
|
||||||
|
/// true(默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
|
||||||
|
/// false — 后台保持连接(适用于桌面端或需要后台实时推送的场景)。
|
||||||
|
/// 设为 false 时,后台仅暂停心跳,不主动断连。
|
||||||
|
final bool disconnectInBackground;
|
||||||
|
|
||||||
/// 日志回调
|
/// 日志回调
|
||||||
final void Function(String message, {String? tag})? onLog;
|
final void Function(String message, {String? tag})? onLog;
|
||||||
|
|
||||||
@@ -104,6 +125,8 @@ class SocketManager {
|
|||||||
required NetworksMessagingApi client,
|
required NetworksMessagingApi client,
|
||||||
required String wsUrl,
|
required String wsUrl,
|
||||||
this.onMessageTransform,
|
this.onMessageTransform,
|
||||||
|
this.onBeforeReconnect,
|
||||||
|
this.disconnectInBackground = true,
|
||||||
this.onCheckNetworkAvailable,
|
this.onCheckNetworkAvailable,
|
||||||
this.onLog,
|
this.onLog,
|
||||||
}) : _client = client,
|
}) : _client = client,
|
||||||
@@ -124,8 +147,8 @@ class SocketManager {
|
|||||||
_reconnectOnForeground = false;
|
_reconnectOnForeground = false;
|
||||||
_reconnectOnNetworkRestore = false;
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
// 前置检查:在后台不连接(省电)
|
// 前置检查:移动端模式下在后台不连接(省电)
|
||||||
if (_isInBackground) {
|
if (_isInBackground && disconnectInBackground) {
|
||||||
_reconnectOnForeground = true;
|
_reconnectOnForeground = true;
|
||||||
_log('In background, defer connect to foreground');
|
_log('In background, defer connect to foreground');
|
||||||
return false;
|
return false;
|
||||||
@@ -165,26 +188,47 @@ class SocketManager {
|
|||||||
/// 当前是否在后台
|
/// 当前是否在后台
|
||||||
bool get isInBackground => _isInBackground;
|
bool get isInBackground => _isInBackground;
|
||||||
|
|
||||||
|
/// Token 热更新
|
||||||
|
///
|
||||||
|
/// 透传给 SocketClient,仅更新内部 token,不断开连接。
|
||||||
|
/// 适用于 HTTP 层 token 刷新后同步到 WebSocket 的场景。
|
||||||
|
void updateToken(String token) {
|
||||||
|
_lastToken = token;
|
||||||
|
_client.updateToken(token);
|
||||||
|
_log('Token updated via SocketManager');
|
||||||
|
}
|
||||||
|
|
||||||
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// 后台 → 断连(省电省流量)
|
// 后台 → 断连(省电省流量)或保持连接(桌面端)
|
||||||
// 前台 → 自动重连(如果之前有连接)
|
// 前台 → 自动重连(如果之前有连接)
|
||||||
|
|
||||||
/// App 进后台 → 断开连接,标记前台恢复时重连
|
/// App 进后台
|
||||||
///
|
///
|
||||||
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
||||||
/// 后台保持连接会消耗电量和流量,断开后由 push 通知兜底。
|
///
|
||||||
|
/// [disconnectInBackground] 为 true 时(默认,移动端):
|
||||||
|
/// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
|
||||||
|
///
|
||||||
|
/// [disconnectInBackground] 为 false 时(桌面端):
|
||||||
|
/// 不断连、不暂停心跳,WebSocket 完全保活。
|
||||||
void onEnterBackground() {
|
void onEnterBackground() {
|
||||||
_isInBackground = true;
|
_isInBackground = true;
|
||||||
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
||||||
_foregroundReconnectTimer?.cancel();
|
_foregroundReconnectTimer?.cancel();
|
||||||
_foregroundReconnectTimer = null;
|
_foregroundReconnectTimer = null;
|
||||||
// 同步 SocketClient 内部状态(与 onEnterForeground 对称)
|
|
||||||
|
if (!disconnectInBackground) {
|
||||||
|
// 桌面端模式:不断连、不暂停心跳,完全保活
|
||||||
|
_log('Entering background, keeping connection alive');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端模式:通知 SocketClient 进后台(暂停心跳)
|
||||||
_client.onEnterBackground();
|
_client.onEnterBackground();
|
||||||
|
|
||||||
if (_lastToken == null) return; // 未登录,无需处理
|
if (_lastToken == null) return; // 未登录,无需处理
|
||||||
|
|
||||||
// 与 _handleNetworkLost 保持一致:
|
|
||||||
// 不仅 connected,connecting / reconnecting 也要断开,
|
// 不仅 connected,connecting / reconnecting 也要断开,
|
||||||
// 防止 SocketClient 在后台继续尝试连接浪费电量和流量。
|
// 防止 SocketClient 在后台继续尝试连接浪费电量和流量。
|
||||||
if (_client.isConnected ||
|
if (_client.isConnected ||
|
||||||
@@ -202,7 +246,11 @@ class SocketManager {
|
|||||||
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
|
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
|
||||||
void onEnterForeground() {
|
void onEnterForeground() {
|
||||||
_isInBackground = false;
|
_isInBackground = false;
|
||||||
|
|
||||||
|
// 只在移动端模式(后台曾断连/暂停心跳)时通知 SocketClient 恢复
|
||||||
|
if (disconnectInBackground) {
|
||||||
_client.onEnterForeground();
|
_client.onEnterForeground();
|
||||||
|
}
|
||||||
|
|
||||||
if (_reconnectOnForeground && _lastToken != null) {
|
if (_reconnectOnForeground && _lastToken != null) {
|
||||||
_reconnectOnForeground = false;
|
_reconnectOnForeground = false;
|
||||||
@@ -226,8 +274,13 @@ class SocketManager {
|
|||||||
_log('Network unavailable, defer reconnect to network restore');
|
_log('Network unavailable, defer reconnect to network restore');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 重连前钩子:刷新即将过期的 token 等
|
||||||
|
await onBeforeReconnect?.call();
|
||||||
|
// token 可能被 onBeforeReconnect 更新(通过 updateToken 链路同步)
|
||||||
|
if (_lastToken != null && !_client.isConnected) {
|
||||||
_client.connect(_wsUrl, token: _lastToken!);
|
_client.connect(_wsUrl, token: _lastToken!);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -275,19 +328,23 @@ class SocketManager {
|
|||||||
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
||||||
_reconnectOnNetworkRestore = false;
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
// 在后台不重连,等前台恢复时再连
|
// 移动端模式:在后台不重连,等前台恢复时再连
|
||||||
if (_isInBackground) {
|
if (_isInBackground && disconnectInBackground) {
|
||||||
_reconnectOnForeground = true;
|
_reconnectOnForeground = true;
|
||||||
_log('Network restored but in background, defer to foreground');
|
_log('Network restored but in background, defer to foreground');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log('Network restored, scheduling reconnect with backoff');
|
_log('Network restored, scheduling reconnect with backoff');
|
||||||
_networkDebouncer.call(() {
|
_networkDebouncer.call(() async {
|
||||||
|
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
||||||
|
// 重连前钩子:刷新即将过期的 token 等
|
||||||
|
await onBeforeReconnect?.call();
|
||||||
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
||||||
_log('Backoff timer fired, reconnecting');
|
_log('Backoff timer fired, reconnecting');
|
||||||
_client.connect(_wsUrl, token: _lastToken!);
|
_client.connect(_wsUrl, token: _lastToken!);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +365,9 @@ class SocketManager {
|
|||||||
/// 原始消息流(不经预处理,调试用)
|
/// 原始消息流(不经预处理,调试用)
|
||||||
Stream<String> get rawMessageStream => _client.rawMessageStream;
|
Stream<String> get rawMessageStream => _client.rawMessageStream;
|
||||||
|
|
||||||
|
/// 二进制消息流
|
||||||
|
Stream<dynamic> get binaryMessageStream => _client.binaryMessageStream;
|
||||||
|
|
||||||
/// 连接状态变化流
|
/// 连接状态变化流
|
||||||
Stream<SocketConnectionState> get connectionStateStream =>
|
Stream<SocketConnectionState> get connectionStateStream =>
|
||||||
_client.connectionStateStream;
|
_client.connectionStateStream;
|
||||||
@@ -333,6 +393,14 @@ class SocketManager {
|
|||||||
return _client.sendString(message);
|
return _client.sendString(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 发送二进制数据
|
||||||
|
///
|
||||||
|
/// 前置检查:未连接或在后台时不发送。
|
||||||
|
Future<bool> sendBytes(List<int> bytes) {
|
||||||
|
if (!_canSend()) return Future.value(false);
|
||||||
|
return _client.sendBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 释放 ──────────────────────────────────────────────────────────────────
|
// ── 释放 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 释放所有资源
|
/// 释放所有资源
|
||||||
@@ -355,7 +423,7 @@ class SocketManager {
|
|||||||
_log('Not connected, cannot send');
|
_log('Not connected, cannot send');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (_isInBackground) {
|
if (_isInBackground && disconnectInBackground) {
|
||||||
_log('In background, skip send');
|
_log('In background, skip send');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
30
apps/im_app/lib/core/ui/base/assets.dart
Normal file
30
apps/im_app/lib/core/ui/base/assets.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/// 静态资源路径常量,统一维护,避免路径字符串散落在业务代码中。
|
||||||
|
///
|
||||||
|
/// 所有路径须与 pubspec.yaml 的 flutter.assets 声明保持一致。
|
||||||
|
/// 新增资源:① 文件放入 assets/ ② pubspec.yaml 声明 ③ 此处加常量。
|
||||||
|
///
|
||||||
|
/// 渲染逻辑(缓存、占位、错误态)由 core/ui/components/ 下的组件负责,不在此处封装。
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
/// ```dart
|
||||||
|
/// Image.asset(AppAssets.logo)
|
||||||
|
/// Image.asset(AppAssets.logo, width: 80, fit: BoxFit.cover)
|
||||||
|
/// ```
|
||||||
|
abstract final class AppAssets {
|
||||||
|
AppAssets._();
|
||||||
|
|
||||||
|
// ── 品牌 ──────────────────────────────────────────────────
|
||||||
|
static const logo = 'assets/images/logo.png';
|
||||||
|
static const logoLight = 'assets/images/logo_light.png';
|
||||||
|
|
||||||
|
// ── 占位图 ────────────────────────────────────────────────
|
||||||
|
static const avatarPlaceholder = 'assets/images/avatar_placeholder.png';
|
||||||
|
|
||||||
|
// ── 空状态插图(SVG,引入 flutter_svg 后启用) ─────────────
|
||||||
|
// static const emptyChat = 'assets/svg/empty_chat.svg';
|
||||||
|
// static const emptyContact = 'assets/svg/empty_contact.svg';
|
||||||
|
// static const emptySearch = 'assets/svg/empty_search.svg';
|
||||||
|
|
||||||
|
// ── 动画 ──────────────────────────────────────────────────
|
||||||
|
// static const loading = 'assets/gif/loading.gif';
|
||||||
|
}
|
||||||
44
apps/im_app/lib/core/ui/base/icons.dart
Normal file
44
apps/im_app/lib/core/ui/base/icons.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 项目图标常量,统一维护,避免 Icons.xxx 散落在业务代码中。
|
||||||
|
///
|
||||||
|
/// 渲染逻辑(大小、颜色、点击态)由调用方负责,不在此处封装。
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
/// ```dart
|
||||||
|
/// Icon(AppIcons.send)
|
||||||
|
/// Icon(AppIcons.send, size: 20, color: Colors.white)
|
||||||
|
/// IconButton(icon: Icon(AppIcons.back), onPressed: ...)
|
||||||
|
/// ```
|
||||||
|
abstract final class AppIcons {
|
||||||
|
AppIcons._();
|
||||||
|
|
||||||
|
// ── 底部导航 ──────────────────────────────────────────────
|
||||||
|
static const chat = Icons.chat_bubble_outline_rounded;
|
||||||
|
static const contact = Icons.people_outline_rounded;
|
||||||
|
static const settings = Icons.settings_outlined;
|
||||||
|
|
||||||
|
// ── 通用操作 ──────────────────────────────────────────────
|
||||||
|
static const back = Icons.arrow_back_ios_new_rounded;
|
||||||
|
static const close = Icons.close_rounded;
|
||||||
|
static const more = Icons.more_horiz_rounded;
|
||||||
|
static const search = Icons.search_rounded;
|
||||||
|
static const add = Icons.add_rounded;
|
||||||
|
|
||||||
|
// ── 聊天输入区 ────────────────────────────────────────────
|
||||||
|
static const send = Icons.send_rounded;
|
||||||
|
static const attach = Icons.attach_file_rounded;
|
||||||
|
static const emoji = Icons.emoji_emotions_outlined;
|
||||||
|
static const camera = Icons.camera_alt_outlined;
|
||||||
|
static const voice = Icons.mic_outlined;
|
||||||
|
|
||||||
|
// ── 用户 / 联系人 ─────────────────────────────────────────
|
||||||
|
static const avatar = Icons.account_circle_outlined;
|
||||||
|
static const addUser = Icons.person_add_outlined;
|
||||||
|
|
||||||
|
// ── 状态反馈 ──────────────────────────────────────────────
|
||||||
|
static const success = Icons.check_circle_outline_rounded;
|
||||||
|
static const warning = Icons.warning_amber_rounded;
|
||||||
|
static const error = Icons.error_outline_rounded;
|
||||||
|
static const info = Icons.info_outline_rounded;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:im_app/app/di/db_provider.dart';
|
import 'package:im_app/app/di/db_provider.dart';
|
||||||
import 'package:im_app/data/local/drift/app_database.dart';
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
@@ -45,7 +44,6 @@ class ChatDbTestState {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ChatDbTestState build() {
|
ChatDbTestState build() {
|
||||||
// 这里就是 onInit
|
// 这里就是 onInit
|
||||||
@@ -98,13 +96,13 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
|||||||
// 让出主线程
|
// 让出主线程
|
||||||
await Future.delayed(Duration.zero);
|
await Future.delayed(Duration.zero);
|
||||||
|
|
||||||
debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)');
|
debugPrint(
|
||||||
|
'已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)',
|
||||||
|
);
|
||||||
|
|
||||||
// 更新 UI 状态
|
// 更新 UI 状态
|
||||||
if (ref.mounted) {
|
if (ref.mounted) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(currentState: '已插入 $completed / $count 条');
|
||||||
currentState: '已插入 $completed / $count 条',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../core/ui/base/context_theme_ext.dart';
|
import '../../../../core/ui/base/context_theme_ext.dart';
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ import '../../../../core/ui/base/context_theme_ext.dart';
|
|||||||
///
|
///
|
||||||
/// 将 [conversationId] 传给对应的 Riverpod `.family` provider 加载完整会话数据。
|
/// 将 [conversationId] 传给对应的 Riverpod `.family` provider 加载完整会话数据。
|
||||||
/// 构造参数保持不变,数据来源从 `extra` 换成 provider 即可。
|
/// 构造参数保持不变,数据来源从 `extra` 换成 provider 即可。
|
||||||
class ChatDetailPage extends StatelessWidget {
|
class ChatDetailPage extends ConsumerWidget {
|
||||||
const ChatDetailPage({
|
const ChatDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.conversationId,
|
required this.conversationId,
|
||||||
@@ -23,7 +24,7 @@ class ChatDetailPage extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final s = context.styles;
|
final s = context.styles;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ class ChatPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final vm = ref.read(chatViewModelProvider.notifier);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('聊天')),
|
appBar: AppBar(title: const Text('聊天')),
|
||||||
body: Center(
|
body: Center(
|
||||||
@@ -32,36 +30,48 @@ class ChatPage extends ConsumerWidget {
|
|||||||
// 切换 Tab:用 go,替换整个历史栈,不可返回
|
// 切换 Tab:用 go,替换整个历史栈,不可返回
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '切换 Tab(go)',
|
label: '切换 Tab(go)',
|
||||||
onPressed: () => vm.goToContact(context),
|
onPressed: () =>
|
||||||
|
ref.read(chatViewModelProvider.notifier).goToContact(context),
|
||||||
),
|
),
|
||||||
// 带参数 push:extra 传 Dart Record,适合已有对象的场景
|
// 带参数 push:extra 传 Dart Record,适合已有对象的场景
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '有参 push(extra)',
|
label: '有参 push(extra)',
|
||||||
onPressed: () => vm.pushChatDetailWithExtra(context),
|
onPressed: () => ref
|
||||||
|
.read(chatViewModelProvider.notifier)
|
||||||
|
.pushChatDetailWithExtra(context),
|
||||||
),
|
),
|
||||||
// 带参数 push:id 内嵌在路径中,适合需要深链接 / 分享的场景
|
// 带参数 push:id 内嵌在路径中,适合需要深链接 / 分享的场景
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '有参 push(路径参数)',
|
label: '有参 push(路径参数)',
|
||||||
onPressed: () => vm.pushChatDetailById(context),
|
onPressed: () => ref
|
||||||
|
.read(chatViewModelProvider.notifier)
|
||||||
|
.pushChatDetailById(context),
|
||||||
),
|
),
|
||||||
// 无参 push:压栈,自动显示返回按钮,不切 Tab
|
// 无参 push:压栈,自动显示返回按钮,不切 Tab
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '无参 push',
|
label: '无参 push',
|
||||||
onPressed: () => vm.pushSettingsTheme(context),
|
onPressed: () => ref
|
||||||
|
.read(chatViewModelProvider.notifier)
|
||||||
|
.pushSettingsTheme(context),
|
||||||
),
|
),
|
||||||
// 无参 go:替换历史,切换到对应 Tab,TabBar 可见,不可返回
|
// 无参 go:替换历史,切换到对应 Tab,TabBar 可见,不可返回
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '无参 go',
|
label: '无参 go',
|
||||||
onPressed: () => vm.goToSettings(context),
|
onPressed: () => ref
|
||||||
|
.read(chatViewModelProvider.notifier)
|
||||||
|
.goToSettings(context),
|
||||||
),
|
),
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: '测试数据库性能',
|
label: '测试数据库性能',
|
||||||
onPressed: () => vm.goToDatabaseTest(context),
|
onPressed: () => ref
|
||||||
|
.read(chatViewModelProvider.notifier)
|
||||||
|
.goToDatabaseTest(context),
|
||||||
),
|
),
|
||||||
AppButton.secondary(
|
AppButton.secondary(
|
||||||
label: '退出登录',
|
label: '退出登录',
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
onPressed: () => vm.logout(),
|
onPressed: () =>
|
||||||
|
ref.read(chatViewModelProvider.notifier).logout(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
/// 联系人页占位
|
/// 联系人页占位
|
||||||
///
|
///
|
||||||
/// 待 contact 功能开发后替换为实际内容。
|
/// 待 contact 功能开发后替换为实际内容。
|
||||||
class ContactPage extends StatelessWidget {
|
class ContactPage extends ConsumerWidget {
|
||||||
const ContactPage({super.key});
|
const ContactPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return const Scaffold();
|
return const Scaffold();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import '../usecases/login_usecase.dart';
|
|||||||
/// ViewModel Provider 由 `@riverpod` 注解自动生成,不在此文件中。
|
/// ViewModel Provider 由 `@riverpod` 注解自动生成,不在此文件中。
|
||||||
///
|
///
|
||||||
/// Auth 模块的 DI 链路:Repository → UseCase(按需)。
|
/// Auth 模块的 DI 链路:Repository → UseCase(按需)。
|
||||||
/// app/di/ 只提供 SDK 基础设施(apiConfig / apiClient / socketManager / storageApi),
|
/// app/di/ 只提供 SDK 基础设施(apiConfig / networkSdkApi / socketManager / storageApi),
|
||||||
/// 业务模块的 Provider 内聚在 features/{模块}/di/ 下。
|
/// 业务模块的 Provider 内聚在 features/{模块}/di/ 下。
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -21,7 +21,7 @@ import '../usecases/login_usecase.dart';
|
|||||||
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
||||||
/// → ref.read(socketManagerProvider) ← app/di/ 手动装配
|
/// → ref.read(socketManagerProvider) ← app/di/ 手动装配
|
||||||
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
|
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
|
||||||
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
|
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
||||||
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
|
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
// TODO: final secureStorage = ref.read(secureStorageProvider);
|
// TODO: final secureStorage = ref.read(secureStorageProvider);
|
||||||
|
|
||||||
return AuthRepositoryImpl(
|
return AuthRepositoryImpl(
|
||||||
client: ref.read(networkSdkApiProvider), // 直接注入 ApiClient
|
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||||
onTokenUpdate: (token) {
|
onTokenUpdate: (token) {
|
||||||
apiConfig.updateToken(token); // 内存(network_sdk)
|
apiConfig.updateToken(token); // 内存(network_sdk)
|
||||||
// TODO: secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
// TODO: secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ part 'login_view_model.g.dart';
|
|||||||
/// loginViewModelProvider ← @riverpod 自动生成(本文件)
|
/// loginViewModelProvider ← @riverpod 自动生成(本文件)
|
||||||
/// → ref.read(loginUseCaseProvider) ← di/ 手动装配
|
/// → ref.read(loginUseCaseProvider) ← di/ 手动装配
|
||||||
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
||||||
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
|
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## 数据流位置
|
/// ## 数据流位置
|
||||||
@@ -56,6 +56,7 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
|
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
|
||||||
Future<void> demoLogin() async {
|
Future<void> demoLogin() async {
|
||||||
final storageApi = ref.read(storageSdkProvider);
|
final storageApi = ref.read(storageSdkProvider);
|
||||||
|
|
||||||
///TODO: StorageSDKLifeCycle 需要只在主项目暴露
|
///TODO: StorageSDKLifeCycle 需要只在主项目暴露
|
||||||
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
||||||
ref.read(authNotifierProvider).login();
|
ref.read(authNotifierProvider).login();
|
||||||
@@ -76,10 +77,9 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
state = state.copyWith(isLoading: true, error: null);
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final user = await ref.read(loginUseCaseProvider).execute(
|
final user = await ref
|
||||||
email: email,
|
.read(loginUseCaseProvider)
|
||||||
password: password,
|
.execute(email: email, password: password);
|
||||||
);
|
|
||||||
|
|
||||||
state = state.copyWith(user: user, isLoading: false);
|
state = state.copyWith(user: user, isLoading: false);
|
||||||
} on FormatException catch (e) {
|
} on FormatException catch (e) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
platform :osx, '11.0'
|
platform :osx, '14.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
@@ -38,5 +38,8 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_macos_build_settings(target)
|
flutter_additional_macos_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['SWIFT_VERSION'] ||= '6.2'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -481,7 +481,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
@@ -557,7 +557,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -580,7 +580,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
@@ -639,7 +639,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -689,7 +689,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
);
|
);
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -732,7 +732,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,9 +37,13 @@ dependencies:
|
|||||||
# 网络状态监听
|
# 网络状态监听
|
||||||
connectivity_plus: ^6.1.0
|
connectivity_plus: ^6.1.0
|
||||||
|
|
||||||
|
# JWT 解析(token 过期检测、主动刷新)
|
||||||
|
dart_jsonwebtoken: ^3.3.2
|
||||||
|
|
||||||
# 数据库(schema 定义在 im_app,连接/CRUD 封装在 storage_sdk)
|
# 数据库(schema 定义在 im_app,连接/CRUD 封装在 storage_sdk)
|
||||||
drift: ^2.22.0
|
drift: ^2.22.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.cipher_guard_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.cipher_guard_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/cipher_guard_sdk/android/build.gradle.kts
Normal file
71
packages/cipher_guard_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.cipher_guard_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.cipher_guard_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class CipherGuardSdkPlugin: NSObject, FlutterPlugin {
|
public class CipherGuardSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -32,15 +32,17 @@ class EncryptionFlutterService {
|
|||||||
|
|
||||||
// Create RSA key generator
|
// Create RSA key generator
|
||||||
final keyGen = RSAKeyGenerator();
|
final keyGen = RSAKeyGenerator();
|
||||||
keyGen.init(ParametersWithRandom(
|
keyGen.init(
|
||||||
|
ParametersWithRandom(
|
||||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 64),
|
RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 64),
|
||||||
secureRandom,
|
secureRandom,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Generate key pair
|
// Generate key pair
|
||||||
final keyPair = keyGen.generateKeyPair();
|
final keyPair = keyGen.generateKeyPair();
|
||||||
final rsaPublicKey = keyPair.publicKey as RSAPublicKey;
|
final rsaPublicKey = keyPair.publicKey;
|
||||||
final rsaPrivateKey = keyPair.privateKey as RSAPrivateKey;
|
final rsaPrivateKey = keyPair.privateKey;
|
||||||
|
|
||||||
// Export to PEM format
|
// Export to PEM format
|
||||||
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
||||||
@@ -186,10 +188,7 @@ class EncryptionFlutterService {
|
|||||||
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
||||||
final key = base64Encode(keyBytes);
|
final key = base64Encode(keyBytes);
|
||||||
|
|
||||||
return SessionKeyResult(
|
return SessionKeyResult(key: key, round: initialRound);
|
||||||
key: key,
|
|
||||||
round: initialRound,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt session key with RSA public key
|
/// Encrypt session key with RSA public key
|
||||||
@@ -263,10 +262,7 @@ class EncryptionFlutterService {
|
|||||||
|
|
||||||
final data = base64Encode(combined);
|
final data = base64Encode(combined);
|
||||||
|
|
||||||
return EncryptedMessageResult(
|
return EncryptedMessageResult(round: round, data: data);
|
||||||
round: round,
|
|
||||||
data: data,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Failed to encrypt message: $e');
|
throw Exception('Failed to encrypt message: $e');
|
||||||
}
|
}
|
||||||
@@ -316,9 +312,7 @@ class EncryptionFlutterService {
|
|||||||
String? _aesSecret;
|
String? _aesSecret;
|
||||||
|
|
||||||
/// Decrypt push notification (AES-GCM)
|
/// Decrypt push notification (AES-GCM)
|
||||||
String decryptPushNotification({
|
String decryptPushNotification({required String encryptedData}) {
|
||||||
required String encryptedData,
|
|
||||||
}) {
|
|
||||||
try {
|
try {
|
||||||
final secret = _aesSecret;
|
final secret = _aesSecret;
|
||||||
if (secret == null) {
|
if (secret == null) {
|
||||||
@@ -440,7 +434,9 @@ class EncryptionFlutterService {
|
|||||||
final len = hex.length;
|
final len = hex.length;
|
||||||
final data = Uint8List(len ~/ 2);
|
final data = Uint8List(len ~/ 2);
|
||||||
for (var i = 0; i < len; i += 2) {
|
for (var i = 0; i < len; i += 2) {
|
||||||
data[i ~/ 2] = (int.parse(hex[i], radix: 16) << 4) + int.parse(hex[i + 1], radix: 16);
|
data[i ~/ 2] =
|
||||||
|
(int.parse(hex[i], radix: 16) << 4) +
|
||||||
|
int.parse(hex[i + 1], radix: 16);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -468,4 +464,3 @@ class EncryptedMessageResult {
|
|||||||
|
|
||||||
EncryptedMessageResult({required this.round, required this.data});
|
EncryptedMessageResult({required this.round, required this.data});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
@@ -23,12 +22,11 @@ allprojects {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
id("kotlin-android")
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.im_log_sdk"
|
namespace = "com.example.im_log_sdk"
|
||||||
|
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -36,17 +34,9 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
java.srcDirs("src/main/kotlin")
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
}
|
|
||||||
getByName("test") {
|
|
||||||
java.srcDirs("src/test/kotlin")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -58,9 +48,7 @@ android {
|
|||||||
isIncludeAndroidResources = true
|
isIncludeAndroidResources = true
|
||||||
all {
|
all {
|
||||||
it.useJUnitPlatform()
|
it.useJUnitPlatform()
|
||||||
|
|
||||||
it.outputs.upToDateWhen { false }
|
it.outputs.upToDateWhen { false }
|
||||||
|
|
||||||
it.testLogging {
|
it.testLogging {
|
||||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
showStandardStreams = true
|
showStandardStreams = true
|
||||||
@@ -70,6 +58,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
@preconcurrency import FlutterMacOS
|
||||||
|
|
||||||
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
||||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
s.dependency 'FlutterMacOS'
|
s.dependency 'FlutterMacOS'
|
||||||
|
|
||||||
s.platform = :osx, '10.11'
|
s.platform = :osx, '14.0'
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.l10n_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.l10n_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/l10n_sdk/android/build.gradle.kts
Normal file
71
packages/l10n_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.l10n_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.l10n_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class L10nSdkPlugin: NSObject, FlutterPlugin {
|
public class L10nSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.media_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.media_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/media_sdk/android/build.gradle.kts
Normal file
71
packages/media_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.media_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.media_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class MediaSdkPlugin: NSObject, FlutterPlugin {
|
public class MediaSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.networks_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.networks_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/networks_sdk/android/build.gradle.kts
Normal file
71
packages/networks_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.networks_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.networks_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class NetworksSdkPlugin: NSObject, FlutterPlugin {
|
public class NetworksSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ export 'src/presentation/facade/networks_messaging_api.dart';
|
|||||||
// Wiring - Implementations
|
// Wiring - Implementations
|
||||||
export 'src/presentation/wiring/networks_messaging_api_impl.dart';
|
export 'src/presentation/wiring/networks_messaging_api_impl.dart';
|
||||||
|
|
||||||
// Dio 类型重导出(App 层上传 / override decodeResponse 需要,避免直接依赖 dio)
|
// Dio 类型重导出(App 层上传 / CancelToken / override decodeResponse 需要,避免直接依赖 dio)
|
||||||
export 'package:dio/dio.dart' show FormData, MultipartFile, Response;
|
export 'package:dio/dio.dart'
|
||||||
|
show FormData, MultipartFile, Response, CancelToken;
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
export 'src/presentation/wiring/api_config.dart';
|
export 'src/presentation/wiring/api_config.dart';
|
||||||
@@ -18,6 +19,7 @@ export 'src/presentation/wiring/network_callbacks.dart';
|
|||||||
export 'src/data/dto/api_requestable.dart';
|
export 'src/data/dto/api_requestable.dart';
|
||||||
export 'src/data/dto/api_response_wrapper.dart';
|
export 'src/data/dto/api_response_wrapper.dart';
|
||||||
export 'src/domain/entities/api_error.dart';
|
export 'src/domain/entities/api_error.dart';
|
||||||
|
export 'src/domain/entities/encrypted_request.dart';
|
||||||
export 'src/domain/entities/http_method.dart';
|
export 'src/domain/entities/http_method.dart';
|
||||||
export 'src/domain/entities/api_request_type.dart';
|
export 'src/domain/entities/api_request_type.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/auth_interceptor.dart';
|
import 'package:networks_sdk/src/data/datasources/http/interceptor/auth_interceptor.dart';
|
||||||
|
import 'package:networks_sdk/src/data/datasources/http/interceptor/encryption_interceptor.dart';
|
||||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/logging_interceptor.dart';
|
import 'package:networks_sdk/src/data/datasources/http/interceptor/logging_interceptor.dart';
|
||||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/retry_interceptor.dart';
|
import 'package:networks_sdk/src/data/datasources/http/interceptor/retry_interceptor.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
|
||||||
/// REST API 客户端
|
/// REST API 客户端
|
||||||
/// 基于 Dio,提供 `executeRequest<T>` 唯一入口
|
/// 基于 Dio,提供请求执行入口
|
||||||
|
///
|
||||||
|
/// 拦截器链顺序:Auth → Encryption → 自定义 → Retry → Logging
|
||||||
///
|
///
|
||||||
/// 使用方式:
|
/// 使用方式:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -28,9 +31,10 @@ class ApiClient {
|
|||||||
receiveTimeout: const Duration(seconds: 60),
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 挂载拦截器(顺序:Auth → 自定义 → Retry → Logging)
|
// 挂载拦截器(顺序:Auth → Encryption → 自定义 → Retry → Logging)
|
||||||
_dio.interceptors.addAll([
|
_dio.interceptors.addAll([
|
||||||
AuthInterceptor(config),
|
AuthInterceptor(config),
|
||||||
|
EncryptionInterceptor(config),
|
||||||
if (additionalInterceptors != null) ...additionalInterceptors,
|
if (additionalInterceptors != null) ...additionalInterceptors,
|
||||||
RetryInterceptor(config: config, dio: _dio),
|
RetryInterceptor(config: config, dio: _dio),
|
||||||
LoggingInterceptor(onLog: config.onLog),
|
LoggingInterceptor(onLog: config.onLog),
|
||||||
@@ -49,13 +53,13 @@ class ApiClient {
|
|||||||
return const ApiError.timeout();
|
return const ApiError.timeout();
|
||||||
case DioExceptionType.connectionError:
|
case DioExceptionType.connectionError:
|
||||||
return const ApiError.noNetworkConnection();
|
return const ApiError.noNetworkConnection();
|
||||||
|
case DioExceptionType.cancel:
|
||||||
|
return const ApiError.cancelled();
|
||||||
default:
|
default:
|
||||||
if (e.response != null) {
|
if (e.response != null) {
|
||||||
return ApiError.apiError(
|
return ApiError.apiError(
|
||||||
code: e.response!.statusCode ?? 0,
|
code: e.response!.statusCode ?? 0,
|
||||||
message: e.response!.statusMessage ??
|
message: e.response!.statusMessage ?? e.message ?? 'Request failed',
|
||||||
e.message ??
|
|
||||||
'Request failed',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ApiError.networkError(e.message ?? 'Network error');
|
return ApiError.networkError(e.message ?? 'Network error');
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
|
||||||
|
/// 加密拦截器(预留给 cipher_guard_sdk)
|
||||||
|
///
|
||||||
|
/// 在拦截器链中位于 Auth 之后、Retry 之前:
|
||||||
|
/// `Auth → Encryption → Custom → Retry → Logging`
|
||||||
|
///
|
||||||
|
/// 回调为 null 时自动跳过,不影响正常请求流程。
|
||||||
|
/// 后续 cipher_guard_sdk 接入后,App 层在 ApiConfig 中注入
|
||||||
|
/// `onEncryptRequest` / `onDecryptResponse` 即可启用加密。
|
||||||
|
///
|
||||||
|
/// ## 加密能力
|
||||||
|
///
|
||||||
|
/// 与简单的 body 加解密不同,本拦截器支持完整的请求改写:
|
||||||
|
/// - 路径加密(如 `/api/login` → `/api/hex(encrypt(login))`)
|
||||||
|
/// - 请求体加密(Map → base64 字符串)
|
||||||
|
/// - Header 注入(X-Token、X-Signature、secret-key 等)
|
||||||
|
/// - Content-Type 覆盖(application/json → text/plain)
|
||||||
|
///
|
||||||
|
/// 加密回调接收原始 path、headers、body,返回 [EncryptedRequest],
|
||||||
|
/// 拦截器根据非 null 字段覆盖请求。
|
||||||
|
class EncryptionInterceptor extends Interceptor {
|
||||||
|
final ApiConfig _config;
|
||||||
|
|
||||||
|
EncryptionInterceptor(this._config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
final encrypt = _config.onEncryptRequest;
|
||||||
|
if (encrypt == null) {
|
||||||
|
handler.next(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 收集当前 headers(转为 Map<String, String>)
|
||||||
|
final currentHeaders = <String, String>{};
|
||||||
|
options.headers.forEach((key, value) {
|
||||||
|
if (value != null) currentHeaders[key] = value.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await encrypt(options.path, currentHeaders, options.data);
|
||||||
|
|
||||||
|
// 根据非 null 字段覆盖请求
|
||||||
|
if (result.path != null) {
|
||||||
|
options.path = result.path!;
|
||||||
|
}
|
||||||
|
if (result.body != null) {
|
||||||
|
options.data = result.body;
|
||||||
|
}
|
||||||
|
if (result.headers != null) {
|
||||||
|
options.headers.addAll(result.headers!);
|
||||||
|
}
|
||||||
|
if (result.contentType != null) {
|
||||||
|
options.contentType = result.contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.onLog?.call(
|
||||||
|
'Request encrypted: ${options.path}',
|
||||||
|
tag: 'Encryption',
|
||||||
|
);
|
||||||
|
|
||||||
|
handler.next(options);
|
||||||
|
} catch (e) {
|
||||||
|
_config.onLog?.call('Request encryption failed: $e', tag: 'Encryption');
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: options,
|
||||||
|
message: 'Request encryption failed: $e',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(Response response, ResponseInterceptorHandler handler) async {
|
||||||
|
final decrypt = _config.onDecryptResponse;
|
||||||
|
if (decrypt == null) {
|
||||||
|
handler.next(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过 null 响应
|
||||||
|
if (response.data == null) {
|
||||||
|
handler.next(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final decrypted = await decrypt(response.data as Object);
|
||||||
|
response.data = decrypted;
|
||||||
|
|
||||||
|
_config.onLog?.call(
|
||||||
|
'Response decrypted: ${response.requestOptions.path}',
|
||||||
|
tag: 'Encryption',
|
||||||
|
);
|
||||||
|
|
||||||
|
handler.next(response);
|
||||||
|
} catch (e) {
|
||||||
|
_config.onLog?.call('Response decryption failed: $e', tag: 'Encryption');
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: response.requestOptions,
|
||||||
|
response: response,
|
||||||
|
message: 'Response decryption failed: $e',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,41 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:networks_sdk/src/data/datasources/http/token_refresh_manager.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
|
||||||
|
|
||||||
/// 重试拦截器
|
/// 重试拦截器
|
||||||
///
|
///
|
||||||
/// 两层重试机制:
|
/// 两层重试机制:
|
||||||
///
|
///
|
||||||
/// 1. **Token 刷新重试**(onResponse)
|
/// 1. **Token 刷新重试**(onResponse)
|
||||||
/// 检测 Token 过期响应 → 触发刷新回调 → 用新 Token 重试原请求
|
/// 检测 Token 过期响应 → 触发 [TokenRefreshManager] → 用新 Token 重试原请求
|
||||||
///
|
///
|
||||||
/// 2. **瞬态错误重试**(onError)
|
/// 2. **瞬态错误重试**(onError)
|
||||||
/// 5xx / 超时 / 连接失败 → 指数退避 + jitter → 自动重试
|
/// 5xx / 超时 / 连接失败 → 指数退避 + jitter → 自动重试
|
||||||
/// 由 [ApiConfig.maxRetries] 控制(默认 0 = 不启用)
|
/// 由 [ApiConfig.maxRetries] 控制(默认 0 = 不启用)
|
||||||
///
|
///
|
||||||
|
/// 另外在 onResponse 中处理强制登出码和业务错误码。
|
||||||
|
///
|
||||||
/// 两层独立运作,可叠加。
|
/// 两层独立运作,可叠加。
|
||||||
class RetryInterceptor extends Interceptor {
|
class RetryInterceptor extends Interceptor {
|
||||||
final ApiConfig config;
|
final ApiConfig config;
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
|
final TokenRefreshManager _tokenManager;
|
||||||
/// Token 刷新锁(防止多个请求同时刷新)
|
|
||||||
bool _isRefreshing = false;
|
|
||||||
Completer<bool>? _refreshCompleter;
|
|
||||||
|
|
||||||
final _random = Random();
|
final _random = Random();
|
||||||
|
|
||||||
RetryInterceptor({required this.config, required this.dio});
|
RetryInterceptor({required this.config, required this.dio})
|
||||||
|
: _tokenManager = TokenRefreshManager(
|
||||||
|
onTokenRefresh: config.onTokenRefresh,
|
||||||
|
onLog: config.onLog,
|
||||||
|
timeout: config.tokenRefreshTimeout,
|
||||||
|
reuseWindow: config.tokenReuseWindow,
|
||||||
|
onGetTokenExpiry: config.onGetTokenExpiry,
|
||||||
|
proactiveRefreshThreshold: config.proactiveRefreshThreshold,
|
||||||
|
);
|
||||||
|
|
||||||
// ── Token 刷新重试 ────────────────────────────────────────────────────────
|
// ── 响应处理(Token 过期 / 强制登出 / 业务错误码)──────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
@@ -40,13 +46,12 @@ class RetryInterceptor extends Interceptor {
|
|||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
final code = _parseCode(data['code']);
|
final code = _parseCode(data['code']);
|
||||||
|
final message = data['message'] as String? ?? '';
|
||||||
|
final requestPath = response.requestOptions.path;
|
||||||
|
|
||||||
// 检查强制登出
|
// 检查强制登出
|
||||||
if (config.forceLogoutCodes.contains(code)) {
|
if (config.forceLogoutCodes.contains(code)) {
|
||||||
config.onLog?.call(
|
config.onLog?.call('Force logout detected (code: $code)', tag: 'Network');
|
||||||
'Force logout detected (code: $code)',
|
|
||||||
tag: 'Network',
|
|
||||||
);
|
|
||||||
config.onForceLogout?.call();
|
config.onForceLogout?.call();
|
||||||
handler.reject(
|
handler.reject(
|
||||||
DioException(
|
DioException(
|
||||||
@@ -58,8 +63,9 @@ class RetryInterceptor extends Interceptor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 Token 过期
|
// 检查 Token 过期(跳过已标记为 token 重试的请求,防止递归)
|
||||||
if (config.tokenExpiredCodes.contains(code)) {
|
if (config.tokenExpiredCodes.contains(code) &&
|
||||||
|
response.requestOptions.extra['_isTokenRetry'] != true) {
|
||||||
config.onLog?.call(
|
config.onLog?.call(
|
||||||
'Token expired (code: $code), refreshing...',
|
'Token expired (code: $code), refreshing...',
|
||||||
tag: 'Network',
|
tag: 'Network',
|
||||||
@@ -68,6 +74,16 @@ class RetryInterceptor extends Interceptor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 业务错误码拦截:非 0 且不在特殊码集合中
|
||||||
|
if (code != 0 && config.onBusinessError != null) {
|
||||||
|
final handled = config.onBusinessError!(code, message, requestPath);
|
||||||
|
if (handled) {
|
||||||
|
// App 层已处理,正常传递响应
|
||||||
|
handler.next(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler.next(response);
|
handler.next(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +92,9 @@ class RetryInterceptor extends Interceptor {
|
|||||||
Response response,
|
Response response,
|
||||||
ResponseInterceptorHandler handler,
|
ResponseInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
final refreshSuccess = await _refreshToken();
|
final newToken = await _tokenManager.refreshIfNeeded();
|
||||||
|
|
||||||
if (!refreshSuccess) {
|
if (newToken == null) {
|
||||||
config.onLog?.call('Token refresh failed', tag: 'Network');
|
config.onLog?.call('Token refresh failed', tag: 'Network');
|
||||||
config.onForceLogout?.call();
|
config.onForceLogout?.call();
|
||||||
handler.reject(
|
handler.reject(
|
||||||
@@ -91,12 +107,14 @@ class RetryInterceptor extends Interceptor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新成功,用新 token 重试原请求
|
// 刷新成功,更新 config 并用新 token 重试原请求
|
||||||
|
config.updateToken(newToken);
|
||||||
config.onLog?.call('Token refreshed, retrying...', tag: 'Network');
|
config.onLog?.call('Token refreshed, retrying...', tag: 'Network');
|
||||||
try {
|
try {
|
||||||
final options = response.requestOptions;
|
final options = response.requestOptions;
|
||||||
// 更新 header 中的 token
|
options.headers['token'] = newToken;
|
||||||
options.headers['token'] = config.token;
|
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
|
||||||
|
options.extra['_isTokenRetry'] = true;
|
||||||
|
|
||||||
final retryResponse = await dio.fetch(options);
|
final retryResponse = await dio.fetch(options);
|
||||||
handler.resolve(retryResponse);
|
handler.resolve(retryResponse);
|
||||||
@@ -105,41 +123,6 @@ class RetryInterceptor extends Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token 刷新(串行锁)
|
|
||||||
/// 多个请求同时过期时,只刷新一次,其余等待
|
|
||||||
Future<bool> _refreshToken() async {
|
|
||||||
if (_isRefreshing) {
|
|
||||||
// 等待正在进行的刷新
|
|
||||||
return _refreshCompleter?.future ?? Future.value(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isRefreshing = true;
|
|
||||||
_refreshCompleter = Completer<bool>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (config.onTokenRefresh == null) {
|
|
||||||
_refreshCompleter!.complete(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final newToken = await config.onTokenRefresh!();
|
|
||||||
final success = newToken != null;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
config.updateToken(newToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshCompleter!.complete(success);
|
|
||||||
return success;
|
|
||||||
} catch (e) {
|
|
||||||
_refreshCompleter!.complete(false);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
_isRefreshing = false;
|
|
||||||
_refreshCompleter = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 瞬态错误重试(指数退避 + jitter)────────────────────────────────────
|
// ── 瞬态错误重试(指数退避 + jitter)────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -198,7 +181,9 @@ class RetryInterceptor extends Interceptor {
|
|||||||
int _backoffDelay(int attempt) {
|
int _backoffDelay(int attempt) {
|
||||||
final baseMs = config.retryBaseDelay.inMilliseconds;
|
final baseMs = config.retryBaseDelay.inMilliseconds;
|
||||||
final exponentialMs = min(baseMs * pow(2, attempt).toInt(), 30000);
|
final exponentialMs = min(baseMs * pow(2, attempt).toInt(), 30000);
|
||||||
final jitterMs = _random.nextInt((exponentialMs * 0.25).toInt().clamp(1, 7500));
|
final jitterMs = _random.nextInt(
|
||||||
|
(exponentialMs * 0.25).toInt().clamp(1, 7500),
|
||||||
|
);
|
||||||
return exponentialMs + jitterMs;
|
return exponentialMs + jitterMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
|
|
||||||
|
/// Token 刷新管理器
|
||||||
|
///
|
||||||
|
/// 两种刷新模式:
|
||||||
|
///
|
||||||
|
/// 1. **被动刷新**([refreshIfNeeded])— 拦截器检测到 token 过期后调用
|
||||||
|
/// 2. **主动刷新**([proactivelyRefreshIfNeeded])— 解析 JWT exp,
|
||||||
|
/// 距过期不足阈值时提前刷新,避免带过期 token 发请求
|
||||||
|
///
|
||||||
|
/// 两种模式共享串行锁和时间窗口保护:
|
||||||
|
/// - **串行锁** — 同一时刻只执行一次刷新,其余请求等待同一 Completer
|
||||||
|
/// - **时间窗口** — 刷新成功后 [reuseWindow] 内再次调用直接返回缓存 token
|
||||||
|
/// - **超时保护** — 刷新回调超过 [timeout] 自动失败,防止死锁
|
||||||
|
class TokenRefreshManager {
|
||||||
|
final OnTokenRefresh? onTokenRefresh;
|
||||||
|
final OnLog? onLog;
|
||||||
|
|
||||||
|
/// 刷新超时时间(防止 onTokenRefresh 卡住导致所有请求阻塞)
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
/// 时间窗口:刷新成功后此时间内再次调用直接返回缓存 token
|
||||||
|
final Duration reuseWindow;
|
||||||
|
|
||||||
|
/// Token 过期时间解析(App 层注入 JWT exp 解析逻辑)
|
||||||
|
final OnGetTokenExpiry? onGetTokenExpiry;
|
||||||
|
|
||||||
|
/// 主动刷新阈值:距过期不足此时间时提前刷新(默认 1 小时)
|
||||||
|
final Duration proactiveRefreshThreshold;
|
||||||
|
|
||||||
|
/// 当前正在进行的刷新任务(null = 空闲)
|
||||||
|
Completer<String?>? _completer;
|
||||||
|
|
||||||
|
/// 上次刷新成功的时间戳
|
||||||
|
DateTime? _lastRefreshTime;
|
||||||
|
|
||||||
|
/// 上次刷新成功的 token(时间窗口内复用)
|
||||||
|
String? _lastToken;
|
||||||
|
|
||||||
|
TokenRefreshManager({
|
||||||
|
this.onTokenRefresh,
|
||||||
|
this.onLog,
|
||||||
|
this.timeout = const Duration(seconds: 10),
|
||||||
|
this.reuseWindow = const Duration(seconds: 3),
|
||||||
|
this.onGetTokenExpiry,
|
||||||
|
this.proactiveRefreshThreshold = const Duration(hours: 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 执行 token 刷新(如果需要)
|
||||||
|
///
|
||||||
|
/// 返回新 token(刷新成功或在时间窗口内),
|
||||||
|
/// 返回 null = 刷新失败或超时。
|
||||||
|
Future<String?> refreshIfNeeded() async {
|
||||||
|
// 1. 时间窗口:最近刷新过且未超时 → 直接返回缓存的 token
|
||||||
|
if (_isWithinReuseWindow()) {
|
||||||
|
_log('Token refreshed recently, reusing');
|
||||||
|
return _lastToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 有正在进行的刷新 → 等待同一 Completer
|
||||||
|
final existing = _completer;
|
||||||
|
if (existing != null) {
|
||||||
|
_log('Waiting for ongoing token refresh');
|
||||||
|
return existing.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 发起新的刷新
|
||||||
|
if (onTokenRefresh == null) {
|
||||||
|
_log('No onTokenRefresh callback configured');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final completer = Completer<String?>();
|
||||||
|
_completer = completer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final newToken = await onTokenRefresh!().timeout(
|
||||||
|
timeout,
|
||||||
|
onTimeout: () {
|
||||||
|
_log('Token refresh timed out after ${timeout.inSeconds}s');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final success = newToken != null && newToken.isNotEmpty;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
_lastRefreshTime = DateTime.now();
|
||||||
|
_lastToken = newToken;
|
||||||
|
_log('Token refreshed successfully');
|
||||||
|
} else {
|
||||||
|
_log('Token refresh failed (null or empty token)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先 complete 再清引用,确保等待者能拿到结果
|
||||||
|
completer.complete(success ? newToken : null);
|
||||||
|
return success ? newToken : null;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Token refresh error: $e');
|
||||||
|
completer.complete(null);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// 清理引用(Completer 已 complete,等待者不受影响)
|
||||||
|
_completer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 token 是否即将过期,是则主动刷新
|
||||||
|
///
|
||||||
|
/// 解析 [currentToken] 的过期时间,距过期不足 [proactiveRefreshThreshold]
|
||||||
|
/// 时调用 [refreshIfNeeded] 刷新。复用串行锁和超时保护。
|
||||||
|
///
|
||||||
|
/// 返回新 token(已刷新)或 null(不需要刷新 / 刷新失败 / 无法解析过期时间)。
|
||||||
|
Future<String?> proactivelyRefreshIfNeeded(String? currentToken) async {
|
||||||
|
if (currentToken == null || onGetTokenExpiry == null) return null;
|
||||||
|
|
||||||
|
final expiry = onGetTokenExpiry!(currentToken);
|
||||||
|
if (expiry == null) return null;
|
||||||
|
|
||||||
|
final remaining = expiry.difference(DateTime.now());
|
||||||
|
if (remaining > proactiveRefreshThreshold) {
|
||||||
|
_log(
|
||||||
|
'Token valid (expires in ${remaining.inMinutes}min), skip proactive refresh',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(
|
||||||
|
'Token expiring soon (${remaining.inMinutes}min left), proactively refreshing',
|
||||||
|
);
|
||||||
|
return refreshIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置状态(登出时调用)
|
||||||
|
void reset() {
|
||||||
|
_lastRefreshTime = null;
|
||||||
|
_lastToken = null;
|
||||||
|
// 不清理 _completer,让正在等待的请求正常结束
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isWithinReuseWindow() {
|
||||||
|
final lastTime = _lastRefreshTime;
|
||||||
|
if (lastTime == null) return false;
|
||||||
|
return DateTime.now().difference(lastTime) < reuseWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
onLog?.call(message, tag: 'TokenRefresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:networks_sdk/src/data/datasources/http/api_client.dart';
|
import 'package:networks_sdk/src/data/datasources/http/api_client.dart';
|
||||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
import '../../../networks_sdk_platform_interface.dart';
|
import '../../../networks_sdk_platform_interface.dart';
|
||||||
import '../../domain/entities/http_method.dart';
|
import '../../domain/entities/http_method.dart';
|
||||||
|
|
||||||
class NetworksSdkMethodChannelDataSource
|
/// 网络层数据源
|
||||||
{
|
///
|
||||||
|
/// 封装 [ApiClient],提供两种请求入口:
|
||||||
|
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||||
|
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||||
|
///
|
||||||
|
/// 流式(SSE)请求也走 [executeRequest],由业务 Request 类 override
|
||||||
|
/// `decodeResponse` 处理 SSE 解析。SDK 内部根据
|
||||||
|
/// `requestType == ApiRequestType.stream` 自动切换 `ResponseType.plain`。
|
||||||
|
class NetworksSdkMethodChannelDataSource {
|
||||||
final NetworksSdkPlatform platform;
|
final NetworksSdkPlatform platform;
|
||||||
|
|
||||||
late ApiClient apiClient;
|
late ApiClient apiClient;
|
||||||
@@ -16,44 +27,51 @@ class NetworksSdkMethodChannelDataSource
|
|||||||
NetworksSdkMethodChannelDataSource(this.platform);
|
NetworksSdkMethodChannelDataSource(this.platform);
|
||||||
|
|
||||||
Future<String?> getPlatformVersion() async {
|
Future<String?> getPlatformVersion() async {
|
||||||
return await getPlatformVersion();
|
return await platform.getPlatformVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void initialize(ApiConfig apiConfig){
|
void initialize(ApiConfig apiConfig) {
|
||||||
apiClient = ApiClient(config: apiConfig);
|
apiClient = ApiClient(config: apiConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行 API 请求 — 唯一入口
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 统一请求入口
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 执行 API 请求 — 统一入口
|
||||||
///
|
///
|
||||||
/// 流程:网络前置检查 → 构建 URL → 设置元数据 → 执行请求 → 解码响应 → 错误映射
|
/// 支持三种请求类型,由 `request.requestType` 控制行为:
|
||||||
/// 拦截器负责:header 注入、Token 刷新重试、日志
|
/// - `request` / `login` — 标准 JSON 请求
|
||||||
|
/// - `upload` — 文件上传(FormData / 二进制)
|
||||||
|
/// - `stream` — SSE / chunked,内部用 `ResponseType.plain` 获取原始文本,
|
||||||
|
/// 由业务 Request 类 override `decodeResponse` 处理 SSE 解析
|
||||||
|
///
|
||||||
|
/// 流程:网络前置检查 → 构建 URL → 设置元数据 → 执行请求
|
||||||
|
/// → 响应变换(可选,stream 类型跳过)→ 解码响应 → 错误映射
|
||||||
|
///
|
||||||
|
/// 拦截器负责:header 注入、加密/解密、Token 刷新重试、业务错误拦截、日志
|
||||||
///
|
///
|
||||||
/// Upload 类型支持两种模式:
|
/// Upload 类型支持两种模式:
|
||||||
/// - 自有后端上传:path 为相对路径,自动拼接 baseURL
|
/// - 自有后端上传:path 为相对路径,自动拼接 baseURL
|
||||||
/// - S3 presigned URL:path 以 http 开头,直接使用全路径
|
/// - S3 presigned URL:path 以 http 开头,直接使用全路径
|
||||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) async {
|
Future<T?> executeRequest<T>(
|
||||||
// 前置检查:网络不可用时直接抛错,避免无效请求
|
ApiRequestable<T> request, {
|
||||||
if (apiClient.config.onCheckNetworkAvailable != null) {
|
CancelToken? cancelToken,
|
||||||
final available = await apiClient.config.onCheckNetworkAvailable!();
|
}) async {
|
||||||
if (!available) {
|
await _checkNetwork(request.path);
|
||||||
apiClient.config.onLog?.call(
|
|
||||||
'Network unavailable, abort request: ${request.path}',
|
|
||||||
tag: 'ApiClient',
|
|
||||||
);
|
|
||||||
throw const ApiError.noNetworkConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload 且 path 以 http 开头 → 直接用全路径(S3 presigned URL)
|
|
||||||
// 否则 → 拼接 baseURL
|
|
||||||
final isUpload = request.requestType == ApiRequestType.upload;
|
final isUpload = request.requestType == ApiRequestType.upload;
|
||||||
|
final isStream = request.requestType == ApiRequestType.stream;
|
||||||
final path = request.path;
|
final path = request.path;
|
||||||
final url = (isUpload && path.startsWith('http')) ? path : '${apiClient.config.baseURL}$path';
|
final url = (isUpload && path.startsWith('http'))
|
||||||
|
? path
|
||||||
|
: '${apiClient.config.baseURL}$path';
|
||||||
|
|
||||||
// 将请求元数据写入 extra,供拦截器读取
|
|
||||||
final options = Options(
|
final options = Options(
|
||||||
method: request.method.value,
|
method: request.method.value,
|
||||||
|
// 流式请求用 plain,Dio 返回原始文本,由 decodeResponse 解析 SSE
|
||||||
|
responseType: isStream ? ResponseType.plain : null,
|
||||||
extra: {
|
extra: {
|
||||||
'requestType': request.requestType,
|
'requestType': request.requestType,
|
||||||
'includeToken': request.includeToken,
|
'includeToken': request.includeToken,
|
||||||
@@ -62,19 +80,22 @@ class NetworksSdkMethodChannelDataSource
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 访问 parameters 触发代码生成器的 fromJson 注册
|
// 访问 parameters 触发代码生成器的 fromJson 注册
|
||||||
// (@ApiRequest 生成的 mixin 在 parameters getter 中注册响应类型)
|
|
||||||
final params = request.parameters;
|
final params = request.parameters;
|
||||||
|
|
||||||
// GET → queryParameters;POST/PUT/DELETE/PATCH → JSON body;Upload → uploadData
|
|
||||||
final isGet = request.method == HttpMethod.get;
|
final isGet = request.method == HttpMethod.get;
|
||||||
final response = await apiClient.dio.request(
|
final response = await apiClient.dio.request(
|
||||||
url,
|
url,
|
||||||
data: isUpload ? request.uploadData : (isGet ? null : params),
|
data: isUpload ? request.uploadData : (isGet ? null : params),
|
||||||
queryParameters: isGet ? params : null,
|
queryParameters: isGet ? params : null,
|
||||||
options: options,
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 解码响应(Upload 类型通常需要 override decodeResponse)
|
// 响应变换:stream 类型由 decodeResponse 自行处理,不做变换
|
||||||
|
if (!isStream) {
|
||||||
|
_applyResponseTransform(response);
|
||||||
|
}
|
||||||
|
|
||||||
return request.decodeResponse(response);
|
return request.decodeResponse(response);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw apiClient.mapDioError(e);
|
throw apiClient.mapDioError(e);
|
||||||
@@ -85,4 +106,162 @@ class NetworksSdkMethodChannelDataSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 文件下载
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 下载文件到本地路径
|
||||||
|
///
|
||||||
|
/// 支持进度回调和断点续传(通过 HTTP Range header 实现)。
|
||||||
|
///
|
||||||
|
/// 非续传模式直接用 Dio.download(高效,内部流式写入)。
|
||||||
|
/// 续传模式用 stream + FileMode.append,因为 Dio.download 始终从
|
||||||
|
/// 文件头部写入,无法正确追加到已下载部分之后。
|
||||||
|
///
|
||||||
|
/// [url] — 下载 URL(完整路径或相对路径,相对路径自动拼接 baseURL)
|
||||||
|
/// [savePath] — 本地保存路径
|
||||||
|
/// [onProgress] — 下载进度回调
|
||||||
|
/// [cancelToken] — 取消令牌
|
||||||
|
/// [resume] — 是否断点续传(文件已存在时从断点继续下载)
|
||||||
|
/// [headers] — 额外请求头
|
||||||
|
Future<void> executeDownload({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
bool resume = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) async {
|
||||||
|
await _checkNetwork(url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final fullUrl = url.startsWith('http')
|
||||||
|
? url
|
||||||
|
: '${apiClient.config.baseURL}$url';
|
||||||
|
|
||||||
|
final extraHeaders = <String, String>{};
|
||||||
|
if (headers != null) extraHeaders.addAll(headers);
|
||||||
|
|
||||||
|
// 断点续传:读取已下载部分的大小,设置 Range header
|
||||||
|
int startBytes = 0;
|
||||||
|
if (resume) {
|
||||||
|
final file = File(savePath);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
startBytes = file.lengthSync();
|
||||||
|
extraHeaders['Range'] = 'bytes=$startBytes-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resume && startBytes > 0) {
|
||||||
|
// 续传模式:stream + append,确保新数据追加到文件末尾
|
||||||
|
await _downloadWithResume(
|
||||||
|
url: fullUrl,
|
||||||
|
savePath: savePath,
|
||||||
|
startBytes: startBytes,
|
||||||
|
headers: extraHeaders,
|
||||||
|
onProgress: onProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 普通下载:Dio.download(高效,内部流式写入)
|
||||||
|
await apiClient.dio.download(
|
||||||
|
fullUrl,
|
||||||
|
savePath,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
deleteOnError: true,
|
||||||
|
options: Options(
|
||||||
|
headers: extraHeaders.isEmpty ? null : extraHeaders,
|
||||||
|
extra: {
|
||||||
|
'requestType': ApiRequestType.download,
|
||||||
|
'includeToken': true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onReceiveProgress: onProgress != null
|
||||||
|
? (received, total) => onProgress(received, total)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw apiClient.mapDioError(e);
|
||||||
|
} on ApiError {
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiError.unknown(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 断点续传下载:stream 响应 + FileMode.append
|
||||||
|
///
|
||||||
|
/// Dio.download 内部用 FileMode.write(从头覆盖),无法正确续传。
|
||||||
|
/// 这里手动读流并追加写入文件。
|
||||||
|
Future<void> _downloadWithResume({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
required int startBytes,
|
||||||
|
required Map<String, String> headers,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) async {
|
||||||
|
final response = await apiClient.dio.get<ResponseBody>(
|
||||||
|
url,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
headers: headers.isEmpty ? null : headers,
|
||||||
|
extra: {'requestType': ApiRequestType.download, 'includeToken': true},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final stream = response.data?.stream;
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
// Content-Length 是本次传输量(不含已下载部分)
|
||||||
|
final contentLength =
|
||||||
|
int.tryParse(response.headers.value('content-length') ?? '') ?? -1;
|
||||||
|
final totalBytes = contentLength > 0 ? contentLength + startBytes : -1;
|
||||||
|
|
||||||
|
final file = File(savePath);
|
||||||
|
final raf = file.openSync(mode: FileMode.writeOnlyAppend);
|
||||||
|
int received = startBytes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await for (final chunk in stream) {
|
||||||
|
raf.writeFromSync(chunk);
|
||||||
|
received += chunk.length;
|
||||||
|
onProgress?.call(received, totalBytes);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
raf.closeSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 内部辅助
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 网络前置检查,不可用时直接抛 [ApiError.noNetworkConnection]
|
||||||
|
Future<void> _checkNetwork(String path) async {
|
||||||
|
if (apiClient.config.onCheckNetworkAvailable != null) {
|
||||||
|
final available = await apiClient.config.onCheckNetworkAvailable!();
|
||||||
|
if (!available) {
|
||||||
|
apiClient.config.onLog?.call(
|
||||||
|
'Network unavailable, abort request: $path',
|
||||||
|
tag: 'ApiClient',
|
||||||
|
);
|
||||||
|
throw const ApiError.noNetworkConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用响应变换(如果 App 层注入了 onTransformResponse)
|
||||||
|
void _applyResponseTransform(Response response) {
|
||||||
|
final transform = apiClient.config.onTransformResponse;
|
||||||
|
if (transform == null) return;
|
||||||
|
if (response.data is! Map<String, dynamic>) return;
|
||||||
|
|
||||||
|
final transformed = transform(response.data as Map<String, dynamic>);
|
||||||
|
if (transformed != null) {
|
||||||
|
response.data = transformed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io' as io;
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:networks_sdk/networks_sdk.dart';
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
@@ -9,10 +11,12 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
|||||||
/// WebSocket 长连接客户端
|
/// WebSocket 长连接客户端
|
||||||
///
|
///
|
||||||
/// 提供:
|
/// 提供:
|
||||||
/// - 连接 / 断连 / 自动重连(指数退避)
|
/// - 连接 / 断连 / 自动重连(指数退避,支持无限重连)
|
||||||
/// - 双层心跳(底层 ping + 应用层 heartbeat)
|
/// - 双层心跳(底层 ping + 应用层 heartbeat)
|
||||||
/// - Stream 输出(消息、连接状态、错误)
|
/// - Stream 输出(JSON 消息、原始字符串、二进制、连接状态、错误)
|
||||||
/// - 生命周期感知(前后台切换)
|
/// - 生命周期感知(前后台切换)
|
||||||
|
/// - Token 热更新(不断连)
|
||||||
|
/// - 消息加密/解密钩子(预留给 cipher_guard_sdk)
|
||||||
///
|
///
|
||||||
/// ## 使用方式
|
/// ## 使用方式
|
||||||
///
|
///
|
||||||
@@ -28,6 +32,9 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
|||||||
/// // 发消息
|
/// // 发消息
|
||||||
/// await client.send({'type': 'chat', 'data': {...}});
|
/// await client.send({'type': 'chat', 'data': {...}});
|
||||||
///
|
///
|
||||||
|
/// // Token 热更新(不断连,下次重连自动使用新 token)
|
||||||
|
/// client.updateToken('new_token');
|
||||||
|
///
|
||||||
/// // 断连
|
/// // 断连
|
||||||
/// await client.disconnect();
|
/// await client.disconnect();
|
||||||
/// ```
|
/// ```
|
||||||
@@ -56,6 +63,7 @@ class SocketClient {
|
|||||||
// ── Stream Controllers ──
|
// ── Stream Controllers ──
|
||||||
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
final _rawMessageController = StreamController<String>.broadcast();
|
final _rawMessageController = StreamController<String>.broadcast();
|
||||||
|
final _binaryMessageController = StreamController<Uint8List>.broadcast();
|
||||||
final _connectionStateController =
|
final _connectionStateController =
|
||||||
StreamController<SocketConnectionState>.broadcast();
|
StreamController<SocketConnectionState>.broadcast();
|
||||||
final _errorController = StreamController<SocketError>.broadcast();
|
final _errorController = StreamController<SocketError>.broadcast();
|
||||||
@@ -93,12 +101,20 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 当前是否已连接
|
/// 当前是否已连接
|
||||||
bool get isConnected =>
|
bool get isConnected => _connectionState == SocketConnectionState.connected;
|
||||||
_connectionState == SocketConnectionState.connected;
|
|
||||||
|
|
||||||
/// 当前连接状态
|
/// 当前连接状态
|
||||||
SocketConnectionState get connectionState => _connectionState;
|
SocketConnectionState get connectionState => _connectionState;
|
||||||
|
|
||||||
|
/// Token 热更新(不断开连接)
|
||||||
|
///
|
||||||
|
/// 仅更新内部持有的 token,下次重连时自动使用新 token。
|
||||||
|
/// 适用于 token 刷新后同步到 WebSocket 的场景。
|
||||||
|
void updateToken(String token) {
|
||||||
|
_currentToken = token;
|
||||||
|
_log('Token updated (no reconnect)');
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// 公开 API — 发送
|
// 公开 API — 发送
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -109,6 +125,8 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 发送原始字符串
|
/// 发送原始字符串
|
||||||
|
///
|
||||||
|
/// 如果配置了 [SocketConfig.onEncryptMessage],发送前自动加密。
|
||||||
Future<bool> sendString(String message) async {
|
Future<bool> sendString(String message) async {
|
||||||
if (!isConnected || _channel == null) {
|
if (!isConnected || _channel == null) {
|
||||||
_emitError(SocketError.sendFailed('Not connected'));
|
_emitError(SocketError.sendFailed('Not connected'));
|
||||||
@@ -116,7 +134,27 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_channel!.sink.add(message);
|
String payload = message;
|
||||||
|
if (config.onEncryptMessage != null) {
|
||||||
|
payload = await config.onEncryptMessage!(message);
|
||||||
|
}
|
||||||
|
_channel!.sink.add(payload);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_emitError(SocketError.sendFailed(e.toString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送二进制数据
|
||||||
|
Future<bool> sendBytes(List<int> bytes) async {
|
||||||
|
if (!isConnected || _channel == null) {
|
||||||
|
_emitError(SocketError.sendFailed('Not connected'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_channel!.sink.add(bytes);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_emitError(SocketError.sendFailed(e.toString()));
|
_emitError(SocketError.sendFailed(e.toString()));
|
||||||
@@ -134,6 +172,9 @@ class SocketClient {
|
|||||||
/// 原始字符串消息流(JSON 解析失败的也走这里)
|
/// 原始字符串消息流(JSON 解析失败的也走这里)
|
||||||
Stream<String> get rawMessageStream => _rawMessageController.stream;
|
Stream<String> get rawMessageStream => _rawMessageController.stream;
|
||||||
|
|
||||||
|
/// 二进制消息流
|
||||||
|
Stream<Uint8List> get binaryMessageStream => _binaryMessageController.stream;
|
||||||
|
|
||||||
/// 连接状态变化流
|
/// 连接状态变化流
|
||||||
Stream<SocketConnectionState> get connectionStateStream =>
|
Stream<SocketConnectionState> get connectionStateStream =>
|
||||||
_connectionStateController.stream;
|
_connectionStateController.stream;
|
||||||
@@ -171,6 +212,7 @@ class SocketClient {
|
|||||||
await _doDisconnect(reason: 'Dispose');
|
await _doDisconnect(reason: 'Dispose');
|
||||||
await _messageController.close();
|
await _messageController.close();
|
||||||
await _rawMessageController.close();
|
await _rawMessageController.close();
|
||||||
|
await _binaryMessageController.close();
|
||||||
await _connectionStateController.close();
|
await _connectionStateController.close();
|
||||||
await _errorController.close();
|
await _errorController.close();
|
||||||
}
|
}
|
||||||
@@ -197,7 +239,16 @@ class SocketClient {
|
|||||||
_log('Connecting to $url');
|
_log('Connecting to $url');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建最终 URL(拼接 token)
|
// 构建最终连接 URL
|
||||||
|
//
|
||||||
|
// 有 onBuildConnectUrl 回调时,App 层完全控制 URL(路径加密、
|
||||||
|
// token 加密、添加 cipher 参数等)。
|
||||||
|
// 无回调时使用默认行为:URL 后追加 ?token=xxx。
|
||||||
|
final String connectUrlString;
|
||||||
|
|
||||||
|
if (config.onBuildConnectUrl != null) {
|
||||||
|
connectUrlString = config.onBuildConnectUrl!(url, _currentToken);
|
||||||
|
} else {
|
||||||
final connectUri = _currentToken != null
|
final connectUri = _currentToken != null
|
||||||
? uri.replace(
|
? uri.replace(
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
@@ -206,15 +257,18 @@ class SocketClient {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: uri;
|
: uri;
|
||||||
|
connectUrlString = connectUri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// 创建 WebSocket 连接
|
// 创建 WebSocket 连接(通过 dart:io 支持压缩选项)
|
||||||
_channel = IOWebSocketChannel.connect(
|
final rawSocket = await io.WebSocket.connect(
|
||||||
connectUri,
|
connectUrlString,
|
||||||
pingInterval: config.pingInterval,
|
compression: config.enableCompression
|
||||||
);
|
? io.CompressionOptions.compressionDefault
|
||||||
|
: io.CompressionOptions.compressionOff,
|
||||||
// 等待连接就绪
|
).timeout(config.connectTimeout);
|
||||||
await _channel!.ready.timeout(config.connectTimeout);
|
rawSocket.pingInterval = config.pingInterval;
|
||||||
|
_channel = IOWebSocketChannel(rawSocket);
|
||||||
|
|
||||||
_log('Connected');
|
_log('Connected');
|
||||||
_updateConnectionState(SocketConnectionState.connected);
|
_updateConnectionState(SocketConnectionState.connected);
|
||||||
@@ -270,26 +324,45 @@ class SocketClient {
|
|||||||
// 内部 — 消息处理
|
// 内部 — 消息处理
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void _handleMessage(dynamic data) {
|
void _handleMessage(dynamic data) async {
|
||||||
|
// 二进制消息
|
||||||
|
if (data is List<int>) {
|
||||||
|
_binaryMessageController.add(
|
||||||
|
data is Uint8List ? data : Uint8List.fromList(data),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data is! String) {
|
if (data is! String) {
|
||||||
// 非字符串消息(如二进制),走 rawMessageStream
|
|
||||||
_rawMessageController.add(data.toString());
|
_rawMessageController.add(data.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 pong 心跳回复
|
// 解密(如果配置了解密回调)
|
||||||
if (data == 'pong') {
|
String text = data;
|
||||||
|
if (config.onDecryptMessage != null) {
|
||||||
|
try {
|
||||||
|
text = await config.onDecryptMessage!(data);
|
||||||
|
} catch (e) {
|
||||||
|
_log('Message decryption failed: $e');
|
||||||
|
_rawMessageController.add(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 pong 心跳回复(解密后检查,加密场景下也能正确匹配)
|
||||||
|
if (text == 'pong') {
|
||||||
_onPongReceived();
|
_onPongReceived();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试 JSON 解析
|
// 尝试 JSON 解析
|
||||||
try {
|
try {
|
||||||
final json = jsonDecode(data) as Map<String, dynamic>;
|
final json = jsonDecode(text) as Map<String, dynamic>;
|
||||||
_messageController.add(json);
|
_messageController.add(json);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// JSON 解析失败,走原始消息流
|
// JSON 解析失败,走原始消息流
|
||||||
_rawMessageController.add(data);
|
_rawMessageController.add(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,11 +401,21 @@ class SocketClient {
|
|||||||
_waitingForPong = false;
|
_waitingForPong = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendPing() {
|
void _sendPing() async {
|
||||||
if (_waitingForPong) return;
|
if (_waitingForPong) return;
|
||||||
|
|
||||||
_waitingForPong = true;
|
_waitingForPong = true;
|
||||||
_channel?.sink.add('ping');
|
|
||||||
|
// 加密场景下 ping 也要加密,与 pong 解密对称
|
||||||
|
String pingPayload = 'ping';
|
||||||
|
if (config.onEncryptMessage != null) {
|
||||||
|
try {
|
||||||
|
pingPayload = await config.onEncryptMessage!('ping');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Ping encryption failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_channel?.sink.add(pingPayload);
|
||||||
|
|
||||||
// 启动 pong 超时计时器
|
// 启动 pong 超时计时器
|
||||||
_pongTimeoutTimer = Timer(config.pongTimeout, () {
|
_pongTimeoutTimer = Timer(config.pongTimeout, () {
|
||||||
@@ -360,7 +443,9 @@ class SocketClient {
|
|||||||
if (_manualDisconnect || !config.autoReconnect || _isBackground) return;
|
if (_manualDisconnect || !config.autoReconnect || _isBackground) return;
|
||||||
if (_connectionState == SocketConnectionState.reconnecting) return;
|
if (_connectionState == SocketConnectionState.reconnecting) return;
|
||||||
|
|
||||||
if (_reconnectAttempts >= config.maxReconnectAttempts) {
|
// 非无限重连模式下检查重连次数上限
|
||||||
|
if (!config.unlimitedReconnect &&
|
||||||
|
_reconnectAttempts >= config.maxReconnectAttempts) {
|
||||||
_log('Max reconnect attempts reached ($_reconnectAttempts)');
|
_log('Max reconnect attempts reached ($_reconnectAttempts)');
|
||||||
_reconnectAttempts = 0;
|
_reconnectAttempts = 0;
|
||||||
return;
|
return;
|
||||||
@@ -375,11 +460,16 @@ class SocketClient {
|
|||||||
pow(2, _reconnectAttempts).toInt() * 1000,
|
pow(2, _reconnectAttempts).toInt() * 1000,
|
||||||
config.maxReconnectDelay.inMilliseconds,
|
config.maxReconnectDelay.inMilliseconds,
|
||||||
);
|
);
|
||||||
final jitterMs = _random.nextInt((baseDelayMs * 0.25).toInt().clamp(1, 7500));
|
final jitterMs = _random.nextInt(
|
||||||
|
(baseDelayMs * 0.25).toInt().clamp(1, 7500),
|
||||||
|
);
|
||||||
final delay = Duration(milliseconds: baseDelayMs + jitterMs);
|
final delay = Duration(milliseconds: baseDelayMs + jitterMs);
|
||||||
|
|
||||||
_log('Reconnecting in ${delay.inMilliseconds}ms '
|
final attemptsInfo = config.unlimitedReconnect
|
||||||
'(attempt $_reconnectAttempts/${config.maxReconnectAttempts})');
|
? 'attempt $_reconnectAttempts/unlimited'
|
||||||
|
: 'attempt $_reconnectAttempts/${config.maxReconnectAttempts}';
|
||||||
|
|
||||||
|
_log('Reconnecting in ${delay.inMilliseconds}ms ($attemptsInfo)');
|
||||||
|
|
||||||
_reconnectTimer = Timer(delay, () async {
|
_reconnectTimer = Timer(delay, () async {
|
||||||
// 重连前检查网络
|
// 重连前检查网络
|
||||||
@@ -393,6 +483,17 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重连前回调(App 层刷新即将过期的 token 等)
|
||||||
|
if (config.onBeforeReconnect != null) {
|
||||||
|
try {
|
||||||
|
await config.onBeforeReconnect!();
|
||||||
|
} catch (e) {
|
||||||
|
_log('onBeforeReconnect failed: $e, skip this reconnect');
|
||||||
|
_startReconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_doConnect();
|
_doConnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:networks_sdk/src/data/datasources/socket/socket_client.dart';
|
import 'package:networks_sdk/src/data/datasources/socket/socket_client.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||||
import 'package:networks_sdk/src/domain/repositories/networks_messaging_repository.dart';
|
import 'package:networks_sdk/src/domain/repositories/networks_messaging_repository.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||||
|
|
||||||
/// Messaging Repository Implementation (Data)
|
/// [NetworksMessagingRepository] 的实现,透传给 [SocketClient]
|
||||||
class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||||
SocketClient? _socketClient;
|
SocketClient? _socketClient;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
@@ -47,6 +49,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
|||||||
return _socketClient!.connectionState;
|
return _socketClient!.connectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateToken(String token) {
|
||||||
|
_checkInitialized();
|
||||||
|
_socketClient!.updateToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> send(Map<String, dynamic> message) {
|
Future<bool> send(Map<String, dynamic> message) {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -59,6 +67,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
|||||||
return _socketClient!.sendString(message);
|
return _socketClient!.sendString(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> sendBytes(List<int> bytes) {
|
||||||
|
_checkInitialized();
|
||||||
|
return _socketClient!.sendBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Map<String, dynamic>> get messageStream {
|
Stream<Map<String, dynamic>> get messageStream {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -71,6 +85,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
|||||||
return _socketClient!.rawMessageStream;
|
return _socketClient!.rawMessageStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> get binaryMessageStream {
|
||||||
|
_checkInitialized();
|
||||||
|
return _socketClient!.binaryMessageStream;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<SocketConnectionState> get connectionStateStream {
|
Stream<SocketConnectionState> get connectionStateStream {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -104,4 +124,3 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
|||||||
_isInitialized = false;
|
_isInitialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
//Repository Impl
|
// Repository Impl
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
|
|
||||||
import '../../domain/repositories/networks_sdk_repository.dart';
|
import '../../domain/repositories/networks_sdk_repository.dart';
|
||||||
import '../datasources/networks_sdk_method_channel_datasource.dart';
|
import '../datasources/networks_sdk_method_channel_datasource.dart';
|
||||||
|
|
||||||
class NetworksSdkRepositoryImpl implements NetworksSdkRepository
|
/// [NetworksSdkRepository] 的实现,透传给 [NetworksSdkMethodChannelDataSource]
|
||||||
{
|
class NetworksSdkRepositoryImpl implements NetworksSdkRepository {
|
||||||
final NetworksSdkMethodChannelDataSource _datasource;
|
final NetworksSdkMethodChannelDataSource _datasource;
|
||||||
|
|
||||||
const NetworksSdkRepositoryImpl(this._datasource);
|
const NetworksSdkRepositoryImpl(this._datasource);
|
||||||
@@ -18,12 +20,34 @@ class NetworksSdkRepositoryImpl implements NetworksSdkRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initialize(ApiConfig apiConfig){
|
void initialize(ApiConfig apiConfig) {
|
||||||
_datasource.initialize(apiConfig);
|
_datasource.initialize(apiConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) {
|
Future<T?> executeRequest<T>(
|
||||||
return _datasource.executeRequest(request);
|
ApiRequestable<T> request, {
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _datasource.executeRequest(request, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> executeDownload({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
bool resume = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) {
|
||||||
|
return _datasource.executeDownload(
|
||||||
|
url: url,
|
||||||
|
savePath: savePath,
|
||||||
|
onProgress: onProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
resume: resume,
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class ApiError with _$ApiError implements Exception {
|
|||||||
required int code,
|
required int code,
|
||||||
required String message,
|
required String message,
|
||||||
}) = _ApiError;
|
}) = _ApiError;
|
||||||
|
|
||||||
|
/// 请求被 CancelToken 取消
|
||||||
|
const factory ApiError.cancelled() = _Cancelled;
|
||||||
const factory ApiError.unknown(String? message) = _Unknown;
|
const factory ApiError.unknown(String? message) = _Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ extension ApiErrorExtension on ApiError {
|
|||||||
networkError: (message) => 'Network error: $message',
|
networkError: (message) => 'Network error: $message',
|
||||||
decodingError: (message) => 'Decoding error: $message',
|
decodingError: (message) => 'Decoding error: $message',
|
||||||
apiError: (code, message) => message,
|
apiError: (code, message) => message,
|
||||||
|
cancelled: () => 'Request cancelled',
|
||||||
unknown: (message) => message ?? 'Unknown error',
|
unknown: (message) => message ?? 'Unknown error',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,13 @@ enum ApiRequestType {
|
|||||||
|
|
||||||
/// 文件上传(multipart,不序列化 parameters)
|
/// 文件上传(multipart,不序列化 parameters)
|
||||||
upload,
|
upload,
|
||||||
|
|
||||||
|
/// 流式请求(SSE / chunked)
|
||||||
|
///
|
||||||
|
/// SDK 内部自动切换 `ResponseType.plain`,Dio 返回原始文本。
|
||||||
|
/// 业务 Request 类 override `decodeResponse` 处理 SSE 解析。
|
||||||
|
stream,
|
||||||
|
|
||||||
|
/// 文件下载(带进度回调,支持断点续传)
|
||||||
|
download,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/// HTTP 请求加密结果
|
||||||
|
///
|
||||||
|
/// 加密回调返回此对象,[EncryptionInterceptor] 根据非 null 字段覆盖原始请求。
|
||||||
|
/// 未设置的字段保持原值不变。
|
||||||
|
///
|
||||||
|
/// 设计依据:HTTP 加密模式下,加密后需要同时修改:
|
||||||
|
/// - 路径(原文 path 加密为 hex 编码)
|
||||||
|
/// - 请求体(JSON body 加密为 base64 字符串,不再是 Map)
|
||||||
|
/// - Headers(添加 X-Token、X-Signature、secret-key 等加密 header)
|
||||||
|
/// - Content-Type(JSON → text/plain)
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // 加密回调返回示例
|
||||||
|
/// EncryptedRequest(
|
||||||
|
/// path: '/api/${hexEncode(encrypt(originalPath))}',
|
||||||
|
/// body: base64Encode(encrypt(jsonBody)),
|
||||||
|
/// headers: {
|
||||||
|
/// 'X-Token': encryptedToken,
|
||||||
|
/// 'X-Signature': signature,
|
||||||
|
/// 'secret-key': clientPublicKey,
|
||||||
|
/// },
|
||||||
|
/// contentType: 'text/plain',
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class EncryptedRequest {
|
||||||
|
/// 加密后的路径
|
||||||
|
///
|
||||||
|
/// null 表示不修改路径。
|
||||||
|
/// 如需加密,拦截器会用此值替换 `RequestOptions.path`。
|
||||||
|
final String? path;
|
||||||
|
|
||||||
|
/// 加密后的请求体
|
||||||
|
///
|
||||||
|
/// null 表示不修改 body。
|
||||||
|
/// 类型不限于 Map — 加密后通常是 base64 字符串或 bytes。
|
||||||
|
final Object? body;
|
||||||
|
|
||||||
|
/// 需要添加或覆盖的 headers
|
||||||
|
///
|
||||||
|
/// null 表示不修改 headers。
|
||||||
|
/// 拦截器会将这些 header 合并到请求中(覆盖同名 header)。
|
||||||
|
final Map<String, String>? headers;
|
||||||
|
|
||||||
|
/// 覆盖 Content-Type
|
||||||
|
///
|
||||||
|
/// null 表示不修改。加密后通常是 `text/plain`(body 已是字符串,非 JSON)。
|
||||||
|
final String? contentType;
|
||||||
|
|
||||||
|
const EncryptedRequest({
|
||||||
|
this.path,
|
||||||
|
this.body,
|
||||||
|
this.headers,
|
||||||
|
this.contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,49 +1,45 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||||
|
|
||||||
/// Messaging Repository Interface (Domain)
|
/// Messaging Repository 接口
|
||||||
abstract class NetworksMessagingRepository {
|
abstract class NetworksMessagingRepository {
|
||||||
/// Initialize with config
|
|
||||||
void initialize(SocketConfig config);
|
void initialize(SocketConfig config);
|
||||||
|
|
||||||
/// Connect to messaging server
|
|
||||||
Future<bool> connect(String url, {String? token});
|
Future<bool> connect(String url, {String? token});
|
||||||
|
|
||||||
/// Disconnect from server
|
|
||||||
Future<void> disconnect();
|
Future<void> disconnect();
|
||||||
|
|
||||||
/// Check if connected
|
|
||||||
bool get isConnected;
|
bool get isConnected;
|
||||||
|
|
||||||
/// Current connection state
|
|
||||||
SocketConnectionState get connectionState;
|
SocketConnectionState get connectionState;
|
||||||
|
|
||||||
/// Send a JSON message
|
/// Token 热更新(不断连)
|
||||||
|
void updateToken(String token);
|
||||||
|
|
||||||
Future<bool> send(Map<String, dynamic> message);
|
Future<bool> send(Map<String, dynamic> message);
|
||||||
|
|
||||||
/// Send a raw string message
|
|
||||||
Future<bool> sendString(String message);
|
Future<bool> sendString(String message);
|
||||||
|
|
||||||
/// Stream of incoming parsed JSON messages
|
/// 发送二进制数据
|
||||||
|
Future<bool> sendBytes(List<int> bytes);
|
||||||
|
|
||||||
Stream<Map<String, dynamic>> get messageStream;
|
Stream<Map<String, dynamic>> get messageStream;
|
||||||
|
|
||||||
/// Stream of raw string messages
|
|
||||||
Stream<String> get rawMessageStream;
|
Stream<String> get rawMessageStream;
|
||||||
|
|
||||||
/// Stream of connection state changes
|
/// 二进制消息流
|
||||||
|
Stream<Uint8List> get binaryMessageStream;
|
||||||
|
|
||||||
Stream<SocketConnectionState> get connectionStateStream;
|
Stream<SocketConnectionState> get connectionStateStream;
|
||||||
|
|
||||||
/// Stream of errors
|
|
||||||
Stream<SocketError> get errorStream;
|
Stream<SocketError> get errorStream;
|
||||||
|
|
||||||
/// Called when app enters foreground
|
|
||||||
void onEnterForeground();
|
void onEnterForeground();
|
||||||
|
|
||||||
/// Called when app enters background
|
|
||||||
void onEnterBackground();
|
void onEnterBackground();
|
||||||
|
|
||||||
/// Dispose all resources
|
|
||||||
Future<void> dispose();
|
Future<void> dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
// Repository Interface(Domain)
|
// Repository Interface(Domain)
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
|
|
||||||
|
/// 网络层 Repository 接口
|
||||||
|
///
|
||||||
|
/// 定义两种请求入口:
|
||||||
|
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||||
|
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||||
abstract class NetworksSdkRepository {
|
abstract class NetworksSdkRepository {
|
||||||
Future<String?> platformVersion();
|
Future<String?> platformVersion();
|
||||||
|
|
||||||
void initialize(ApiConfig apiConfig);
|
void initialize(ApiConfig apiConfig);
|
||||||
|
|
||||||
Future<T?> executeRequest<T>(ApiRequestable<T> request);
|
Future<T?> executeRequest<T>(
|
||||||
|
ApiRequestable<T> request, {
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 文件下载(支持进度回调和断点续传)
|
||||||
|
Future<void> executeDownload({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
bool resume = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,92 +1,75 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||||
|
|
||||||
/// Messaging API for real-time communication
|
/// 实时通信公开接口
|
||||||
///
|
///
|
||||||
/// This abstract class provides a technology-agnostic interface for
|
/// 底层基于 WebSocket,支持 JSON / 字符串 / 二进制消息、
|
||||||
/// real-time messaging. The actual implementation may use WebSocket
|
/// 自动重连(含无限重连)、Token 热更新、消息加密/解密钩子。
|
||||||
/// or other transport mechanisms.
|
|
||||||
///
|
///
|
||||||
/// ## Usage
|
/// ## 使用方式
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final messaging = NetworksMessagingApi();
|
/// final messaging = NetworksMessagingApi();
|
||||||
/// await messaging.initialize(SocketConfig(...));
|
/// await messaging.initialize(SocketConfig(...));
|
||||||
///
|
///
|
||||||
/// // Connect to messaging server
|
|
||||||
/// await messaging.connect('wss://api.example.com/ws', token: 'xxx');
|
/// await messaging.connect('wss://api.example.com/ws', token: 'xxx');
|
||||||
///
|
///
|
||||||
/// // Listen for messages
|
|
||||||
/// messaging.messageStream.listen((msg) => print(msg));
|
/// messaging.messageStream.listen((msg) => print(msg));
|
||||||
///
|
///
|
||||||
/// // Send messages
|
|
||||||
/// await messaging.send({'type': 'chat', 'data': {...}});
|
/// await messaging.send({'type': 'chat', 'data': {...}});
|
||||||
///
|
///
|
||||||
/// // Handle connection state
|
/// // Token 热更新(不断连)
|
||||||
/// messaging.connectionStateStream.listen((state) => ...);
|
/// messaging.updateToken('new_token');
|
||||||
///
|
///
|
||||||
/// // Handle errors
|
/// // 发送二进制
|
||||||
/// messaging.errorStream.listen((error) => ...);
|
/// await messaging.sendBytes(Uint8List.fromList([0x01, 0x02]));
|
||||||
///
|
///
|
||||||
/// // Lifecycle management
|
|
||||||
/// messaging.onEnterForeground();
|
|
||||||
/// messaging.onEnterBackground();
|
|
||||||
///
|
|
||||||
/// // Cleanup
|
|
||||||
/// await messaging.disconnect();
|
/// await messaging.disconnect();
|
||||||
/// await messaging.dispose();
|
/// await messaging.dispose();
|
||||||
/// ```
|
/// ```
|
||||||
abstract class NetworksMessagingApi
|
abstract class NetworksMessagingApi {
|
||||||
{
|
|
||||||
factory NetworksMessagingApi() => NetworksSdkWiring.buildMessagingApi();
|
factory NetworksMessagingApi() => NetworksSdkWiring.buildMessagingApi();
|
||||||
|
|
||||||
/// Initialize the messaging service with configuration
|
|
||||||
void initialize(SocketConfig config);
|
void initialize(SocketConfig config);
|
||||||
|
|
||||||
/// Connect to the messaging server
|
|
||||||
///
|
|
||||||
/// [url] - WebSocket URL (e.g., 'wss://api.example.com/ws')
|
|
||||||
/// [token] - Optional authentication token
|
|
||||||
Future<bool> connect(String url, {String? token});
|
Future<bool> connect(String url, {String? token});
|
||||||
|
|
||||||
/// Disconnect from the messaging server
|
|
||||||
///
|
|
||||||
/// Manual disconnect does not trigger auto-reconnect
|
|
||||||
Future<void> disconnect();
|
Future<void> disconnect();
|
||||||
|
|
||||||
/// Check if currently connected
|
|
||||||
bool get isConnected;
|
bool get isConnected;
|
||||||
|
|
||||||
/// Current connection state
|
|
||||||
SocketConnectionState get connectionState;
|
SocketConnectionState get connectionState;
|
||||||
|
|
||||||
/// Send a JSON message
|
/// Token 热更新(不断开连接)
|
||||||
|
///
|
||||||
|
/// 仅更新内部 token,下次重连自动使用新 token。
|
||||||
|
void updateToken(String token);
|
||||||
|
|
||||||
Future<bool> send(Map<String, dynamic> message);
|
Future<bool> send(Map<String, dynamic> message);
|
||||||
|
|
||||||
/// Send a raw string message
|
|
||||||
Future<bool> sendString(String message);
|
Future<bool> sendString(String message);
|
||||||
|
|
||||||
/// Stream of incoming parsed JSON messages
|
/// 发送二进制数据
|
||||||
|
Future<bool> sendBytes(List<int> bytes);
|
||||||
|
|
||||||
Stream<Map<String, dynamic>> get messageStream;
|
Stream<Map<String, dynamic>> get messageStream;
|
||||||
|
|
||||||
/// Stream of raw string messages (including failed JSON parses)
|
|
||||||
Stream<String> get rawMessageStream;
|
Stream<String> get rawMessageStream;
|
||||||
|
|
||||||
/// Stream of connection state changes
|
/// 二进制消息流
|
||||||
|
Stream<Uint8List> get binaryMessageStream;
|
||||||
|
|
||||||
Stream<SocketConnectionState> get connectionStateStream;
|
Stream<SocketConnectionState> get connectionStateStream;
|
||||||
|
|
||||||
/// Stream of errors
|
|
||||||
Stream<SocketError> get errorStream;
|
Stream<SocketError> get errorStream;
|
||||||
|
|
||||||
/// Called when app enters foreground
|
|
||||||
void onEnterForeground();
|
void onEnterForeground();
|
||||||
|
|
||||||
/// Called when app enters background
|
|
||||||
void onEnterBackground();
|
void onEnterBackground();
|
||||||
|
|
||||||
/// Dispose all resources
|
|
||||||
Future<void> dispose();
|
Future<void> dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,69 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
|
||||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
||||||
|
|
||||||
|
/// Networks SDK 公开接口
|
||||||
/// SDK API
|
///
|
||||||
abstract class NetworksSdkApi
|
/// 提供两种请求入口:
|
||||||
{
|
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||||
|
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||||
|
///
|
||||||
|
/// 流式请求(SSE)也走 [executeRequest],由业务 Request 类 override
|
||||||
|
/// `decodeResponse` 处理 SSE 解析。SDK 根据 `requestType == stream`
|
||||||
|
/// 自动切换响应类型。
|
||||||
|
///
|
||||||
|
/// 使用方式:
|
||||||
|
/// ```dart
|
||||||
|
/// final sdk = NetworksSdkApi();
|
||||||
|
/// sdk.initialize(apiConfig);
|
||||||
|
///
|
||||||
|
/// // 标准请求
|
||||||
|
/// final data = await sdk.executeRequest(LoginRequest(...));
|
||||||
|
///
|
||||||
|
/// // 流式请求(SSE)— 同一入口,Request 类 override decodeResponse
|
||||||
|
/// final result = await sdk.executeRequest(VoiceTranscribeRequest(...));
|
||||||
|
///
|
||||||
|
/// // 文件下载
|
||||||
|
/// await sdk.executeDownload(
|
||||||
|
/// url: '/files/report.pdf',
|
||||||
|
/// savePath: '/tmp/report.pdf',
|
||||||
|
/// onProgress: (received, total) => print('$received / $total'),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
abstract class NetworksSdkApi {
|
||||||
factory NetworksSdkApi() => NetworksSdkWiring.build();
|
factory NetworksSdkApi() => NetworksSdkWiring.build();
|
||||||
|
|
||||||
Future<String?> platformVersion();
|
Future<String?> platformVersion();
|
||||||
|
|
||||||
void initialize(ApiConfig aApiConfig);
|
void initialize(ApiConfig apiConfig);
|
||||||
|
|
||||||
Future<T?> executeRequest<T>(ApiRequestable<T> request);
|
/// 执行 API 请求 — 统一入口
|
||||||
|
///
|
||||||
|
/// 支持标准请求、登录、上传、流式(SSE),由 `request.requestType` 控制。
|
||||||
|
/// 流式请求由业务 Request 类 override `decodeResponse` 处理 SSE 解析。
|
||||||
|
///
|
||||||
|
/// [cancelToken] — 可选,用于取消正在进行的请求
|
||||||
|
Future<T?> executeRequest<T>(
|
||||||
|
ApiRequestable<T> request, {
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 下载文件到本地路径
|
||||||
|
///
|
||||||
|
/// [url] — 下载 URL(完整路径或相对路径)
|
||||||
|
/// [savePath] — 本地保存路径
|
||||||
|
/// [onProgress] — 下载进度回调
|
||||||
|
/// [cancelToken] — 取消令牌
|
||||||
|
/// [resume] — 是否断点续传
|
||||||
|
/// [headers] — 额外请求头
|
||||||
|
Future<void> executeDownload({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
bool resume = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'network_callbacks.dart';
|
import 'network_callbacks.dart';
|
||||||
|
|
||||||
/// API 配置
|
/// API 配置
|
||||||
@@ -13,12 +12,22 @@ class ApiConfig {
|
|||||||
/// 平台相关 headers(App 层注入:version、platform、channel 等)
|
/// 平台相关 headers(App 层注入:version、platform、channel 等)
|
||||||
Map<String, String> platformHeaders;
|
Map<String, String> platformHeaders;
|
||||||
|
|
||||||
|
// ── 认证回调 ──
|
||||||
|
|
||||||
/// Token 过期时的刷新回调
|
/// Token 过期时的刷新回调
|
||||||
final OnTokenRefresh? onTokenRefresh;
|
final OnTokenRefresh? onTokenRefresh;
|
||||||
|
|
||||||
/// 需要强制登出时的回调
|
/// 需要强制登出时的回调
|
||||||
final OnForceLogout? onForceLogout;
|
final OnForceLogout? onForceLogout;
|
||||||
|
|
||||||
|
/// Token 更新后的通知回调
|
||||||
|
///
|
||||||
|
/// 在 [updateToken] 被调用且新 token 非空时触发。
|
||||||
|
/// App 层用于同步 token 到 WebSocket 等其他模块。
|
||||||
|
final void Function(String newToken)? onTokenUpdated;
|
||||||
|
|
||||||
|
// ── 基础回调 ──
|
||||||
|
|
||||||
/// 日志输出回调(不设置则不输出日志)
|
/// 日志输出回调(不设置则不输出日志)
|
||||||
final OnLog? onLog;
|
final OnLog? onLog;
|
||||||
|
|
||||||
@@ -29,12 +38,39 @@ class ApiConfig {
|
|||||||
/// 返回 false 则直接抛 [ApiError.noNetworkConnection],不走网络。
|
/// 返回 false 则直接抛 [ApiError.noNetworkConnection],不走网络。
|
||||||
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
|
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
|
||||||
|
|
||||||
|
// ── 加密回调(预留给 cipher_guard_sdk)──
|
||||||
|
|
||||||
|
/// 请求体加密回调,null 时不加密
|
||||||
|
final OnEncryptRequest? onEncryptRequest;
|
||||||
|
|
||||||
|
/// 响应体解密回调,null 时不解密
|
||||||
|
final OnDecryptResponse? onDecryptResponse;
|
||||||
|
|
||||||
|
// ── 业务错误回调 ──
|
||||||
|
|
||||||
|
/// 业务错误拦截回调
|
||||||
|
///
|
||||||
|
/// 在 token 过期 / 强制登出判断之后执行。
|
||||||
|
/// 返回 true = App 层已处理,SDK 正常传递响应;
|
||||||
|
/// 返回 false = 未处理,SDK 继续正常流程。
|
||||||
|
final OnBusinessError? onBusinessError;
|
||||||
|
|
||||||
|
/// 响应变换回调
|
||||||
|
///
|
||||||
|
/// 在 `executeRequest` 解码前调用,App 层可以统一解包
|
||||||
|
/// `{ code, data, message }` 结构。返回 null 表示不变换。
|
||||||
|
final OnTransformResponse? onTransformResponse;
|
||||||
|
|
||||||
|
// ── 错误码集合 ──
|
||||||
|
|
||||||
/// App 层定义的 Token 过期错误码集合
|
/// App 层定义的 Token 过期错误码集合
|
||||||
final Set<int> tokenExpiredCodes;
|
final Set<int> tokenExpiredCodes;
|
||||||
|
|
||||||
/// App 层定义的强制登出错误码集合
|
/// App 层定义的强制登出错误码集合
|
||||||
final Set<int> forceLogoutCodes;
|
final Set<int> forceLogoutCodes;
|
||||||
|
|
||||||
|
// ── 重试配置 ──
|
||||||
|
|
||||||
/// 瞬态错误最大重试次数(5xx / 超时 / 连接失败)
|
/// 瞬态错误最大重试次数(5xx / 超时 / 连接失败)
|
||||||
///
|
///
|
||||||
/// 0 = 不重试(默认),设为 3 启用重试。
|
/// 0 = 不重试(默认),设为 3 启用重试。
|
||||||
@@ -46,18 +82,50 @@ class ApiConfig {
|
|||||||
/// 实际延迟 = min(baseDelay * 2^attempt, 30s) + jitter
|
/// 实际延迟 = min(baseDelay * 2^attempt, 30s) + jitter
|
||||||
final Duration retryBaseDelay;
|
final Duration retryBaseDelay;
|
||||||
|
|
||||||
|
// ── Token 刷新配置 ──
|
||||||
|
|
||||||
|
/// Token 刷新超时时间,防止 onTokenRefresh 卡住导致请求永远阻塞
|
||||||
|
final Duration tokenRefreshTimeout;
|
||||||
|
|
||||||
|
/// Token 刷新时间窗口:刷新成功后此时间内再次收到过期码直接返回成功,
|
||||||
|
/// 避免服务端同步延迟导致的误判
|
||||||
|
final Duration tokenReuseWindow;
|
||||||
|
|
||||||
|
// ── 主动刷新配置 ──
|
||||||
|
|
||||||
|
/// Token 过期时间解析回调
|
||||||
|
///
|
||||||
|
/// App 层解析 JWT `exp` claim,用于主动刷新判断。
|
||||||
|
/// 未注入时不启用主动刷新。
|
||||||
|
final OnGetTokenExpiry? onGetTokenExpiry;
|
||||||
|
|
||||||
|
/// 主动刷新阈值:距过期不足此时间时提前刷新
|
||||||
|
///
|
||||||
|
/// 默认 1 小时。WebSocket 重连前、App 回前台时
|
||||||
|
/// 自动检查并刷新即将过期的 token,避免带过期 token 发起请求。
|
||||||
|
final Duration proactiveRefreshThreshold;
|
||||||
|
|
||||||
ApiConfig({
|
ApiConfig({
|
||||||
required this.baseURL,
|
required this.baseURL,
|
||||||
this.token,
|
this.token,
|
||||||
this.platformHeaders = const {},
|
this.platformHeaders = const {},
|
||||||
this.onTokenRefresh,
|
this.onTokenRefresh,
|
||||||
this.onForceLogout,
|
this.onForceLogout,
|
||||||
|
this.onTokenUpdated,
|
||||||
this.onLog,
|
this.onLog,
|
||||||
this.onCheckNetworkAvailable,
|
this.onCheckNetworkAvailable,
|
||||||
|
this.onEncryptRequest,
|
||||||
|
this.onDecryptResponse,
|
||||||
|
this.onBusinessError,
|
||||||
|
this.onTransformResponse,
|
||||||
this.tokenExpiredCodes = const {},
|
this.tokenExpiredCodes = const {},
|
||||||
this.forceLogoutCodes = const {},
|
this.forceLogoutCodes = const {},
|
||||||
this.maxRetries = 0,
|
this.maxRetries = 0,
|
||||||
this.retryBaseDelay = const Duration(seconds: 1),
|
this.retryBaseDelay = const Duration(seconds: 1),
|
||||||
|
this.tokenRefreshTimeout = const Duration(seconds: 10),
|
||||||
|
this.tokenReuseWindow = const Duration(seconds: 3),
|
||||||
|
this.onGetTokenExpiry,
|
||||||
|
this.proactiveRefreshThreshold = const Duration(hours: 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 构建默认 headers
|
/// 构建默认 headers
|
||||||
@@ -70,6 +138,8 @@ class ApiConfig {
|
|||||||
final headers = <String, String>{
|
final headers = <String, String>{
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
'Keep-Alive': 'timeout=60',
|
||||||
|
// Unix 时间戳(秒),整数值,非格式化日期字符串
|
||||||
'Timestamp': '${DateTime.now().millisecondsSinceEpoch ~/ 1000}',
|
'Timestamp': '${DateTime.now().millisecondsSinceEpoch ~/ 1000}',
|
||||||
'APP-Request-ID': _generateRequestId(),
|
'APP-Request-ID': _generateRequestId(),
|
||||||
};
|
};
|
||||||
@@ -91,8 +161,13 @@ class ApiConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 更新 token
|
/// 更新 token
|
||||||
|
///
|
||||||
|
/// 同时触发 [onTokenUpdated] 通知其他模块(如 WebSocket)同步 token。
|
||||||
void updateToken(String? newToken) {
|
void updateToken(String? newToken) {
|
||||||
token = newToken;
|
token = newToken;
|
||||||
|
if (newToken != null && newToken.isNotEmpty) {
|
||||||
|
onTokenUpdated?.call(newToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新 base URL
|
/// 更新 base URL
|
||||||
|
|||||||
@@ -1,13 +1,105 @@
|
|||||||
/// 网络层回调类型定义,由 App 层注入 SDK,避免 SDK 直接依赖外部实现。
|
/// 网络层回调类型定义,由 App 层注入 SDK,避免 SDK 直接依赖外部实现。
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:networks_sdk/src/domain/entities/encrypted_request.dart';
|
||||||
|
|
||||||
|
// ── 认证 ──
|
||||||
|
|
||||||
|
/// Token 刷新回调,返回新 token;返回 null 表示刷新失败
|
||||||
typedef OnTokenRefresh = Future<String?> Function();
|
typedef OnTokenRefresh = Future<String?> Function();
|
||||||
|
|
||||||
/// 強制登出回調
|
/// 强制登出回调
|
||||||
typedef OnForceLogout = void Function();
|
typedef OnForceLogout = void Function();
|
||||||
|
|
||||||
/// 日誌輸出回調
|
// ── Token 生命周期 ──
|
||||||
|
|
||||||
|
/// 获取 token 过期时间
|
||||||
|
///
|
||||||
|
/// App 层解析 JWT 的 `exp` claim 返回过期时间。
|
||||||
|
/// 返回 null 表示无法解析(非 JWT 或格式错误)。
|
||||||
|
typedef OnGetTokenExpiry = DateTime? Function(String token);
|
||||||
|
|
||||||
|
// ── 基础 ──
|
||||||
|
|
||||||
|
/// 日志输出回调
|
||||||
typedef OnLog = void Function(String message, {String? tag});
|
typedef OnLog = void Function(String message, {String? tag});
|
||||||
|
|
||||||
/// 網路可用性查詢(App 層注入,SDK 在請求前調用)
|
/// 网络可用性查询(App 层注入,SDK 在请求前调用)
|
||||||
typedef OnCheckNetworkAvailable = Future<bool> Function();
|
typedef OnCheckNetworkAvailable = Future<bool> Function();
|
||||||
|
|
||||||
|
// ── 加密(预留给 cipher_guard_sdk)──
|
||||||
|
|
||||||
|
/// HTTP 请求加密回调
|
||||||
|
///
|
||||||
|
/// 接收原始路径、headers、请求体,返回 [EncryptedRequest]。
|
||||||
|
/// 拦截器根据返回值中的非 null 字段覆盖原始请求。
|
||||||
|
///
|
||||||
|
/// 参数说明:
|
||||||
|
/// - [path] — 原始请求路径(如 `/api/v1/auth/login`)
|
||||||
|
/// - [headers] — 当前请求的全部 headers(含 token、platform headers 等)
|
||||||
|
/// - [body] — 原始请求体(可能是 Map、String、null 等)
|
||||||
|
///
|
||||||
|
/// App 层实现示例(X25519 + AES-256-CBC 模式):
|
||||||
|
/// - 加密 path → hex 编码 → 替换路径
|
||||||
|
/// - 加密 body → base64 编码 → 替换请求体
|
||||||
|
/// - 加密 token → 放入 X-Token header
|
||||||
|
/// - Ed25519 签名 → 放入 X-Signature header
|
||||||
|
/// - Content-Type → text/plain
|
||||||
|
typedef OnEncryptRequest =
|
||||||
|
Future<EncryptedRequest> Function(
|
||||||
|
String path,
|
||||||
|
Map<String, String> headers,
|
||||||
|
Object? body,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// HTTP 响应解密回调
|
||||||
|
///
|
||||||
|
/// 输入是原始响应数据(加密后可能是 String、`List<int>`、或 Map),
|
||||||
|
/// 返回解密后的 Map 供业务层使用。
|
||||||
|
///
|
||||||
|
/// [responseData] 的实际类型取决于服务端响应格式:
|
||||||
|
/// - 加密模式下通常是 base64 字符串
|
||||||
|
/// - 非加密模式下是 `Map<String, dynamic>`
|
||||||
|
typedef OnDecryptResponse =
|
||||||
|
Future<Map<String, dynamic>> Function(Object responseData);
|
||||||
|
|
||||||
|
// ── 业务错误 ──
|
||||||
|
|
||||||
|
/// 业务错误拦截回调
|
||||||
|
///
|
||||||
|
/// App 层统一处理特定错误码,返回 true = 已处理(SDK 不再抛错),
|
||||||
|
/// 返回 false = 未处理(SDK 继续正常流程)。
|
||||||
|
typedef OnBusinessError = bool Function(int code, String message, String path);
|
||||||
|
|
||||||
|
/// 响应变换回调
|
||||||
|
///
|
||||||
|
/// App 层自定义响应解包逻辑(如统一解包 `{ code, data, message }` 结构)。
|
||||||
|
/// 返回 null 表示不变换,使用原始响应。
|
||||||
|
typedef OnTransformResponse =
|
||||||
|
Map<String, dynamic>? Function(Map<String, dynamic> raw);
|
||||||
|
|
||||||
|
// ── 下载 ──
|
||||||
|
|
||||||
|
/// 下载进度回调
|
||||||
|
typedef OnDownloadProgress = void Function(int received, int total);
|
||||||
|
|
||||||
|
// ── WebSocket 加密(预留给 cipher_guard_sdk)──
|
||||||
|
|
||||||
|
/// WebSocket 连接 URL 构建回调
|
||||||
|
///
|
||||||
|
/// 建立连接前调用,接收原始 URL 和 token,返回最终的连接 URL 字符串。
|
||||||
|
/// WS 握手本质是 HTTP GET 升级请求,只需控制 URL(路径 + 查询参数)。
|
||||||
|
///
|
||||||
|
/// App 层可在此(通过调用 cipher_guard_sdk):
|
||||||
|
/// - 加密 URL 路径(如 `/ws` → `/hex(encrypt(ws))`)
|
||||||
|
/// - 加密 token 参数(明文 token 不出现在 URL 中)
|
||||||
|
/// - 添加加密模式协商参数(如 `cipher=true&type=mode3`)
|
||||||
|
///
|
||||||
|
/// null 时使用默认行为:在 URL 后追加 `?token=xxx`。
|
||||||
|
typedef OnBuildConnectUrl = String Function(String url, String? token);
|
||||||
|
|
||||||
|
/// WebSocket 发送前加密回调
|
||||||
|
typedef OnEncryptMessage = Future<String> Function(String plainText);
|
||||||
|
|
||||||
|
/// WebSocket 收到后解密回调
|
||||||
|
typedef OnDecryptMessage = Future<String> Function(String cipherText);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:networks_sdk/src/data/repositories/networks_messaging_repository_impl.dart';
|
import 'package:networks_sdk/src/data/repositories/networks_messaging_repository_impl.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||||
@@ -5,7 +7,7 @@ import 'package:networks_sdk/src/domain/repositories/networks_messaging_reposito
|
|||||||
import 'package:networks_sdk/src/presentation/facade/networks_messaging_api.dart';
|
import 'package:networks_sdk/src/presentation/facade/networks_messaging_api.dart';
|
||||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||||
|
|
||||||
/// Implementation of [NetworksMessagingApi] using [NetworksMessagingRepository]
|
/// [NetworksMessagingApi] 的实现,透传给 [NetworksMessagingRepository]
|
||||||
class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||||
NetworksMessagingRepository? _repository;
|
NetworksMessagingRepository? _repository;
|
||||||
|
|
||||||
@@ -47,6 +49,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
|||||||
return _repository!.connectionState;
|
return _repository!.connectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateToken(String token) {
|
||||||
|
_checkInitialized();
|
||||||
|
_repository!.updateToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> send(Map<String, dynamic> message) {
|
Future<bool> send(Map<String, dynamic> message) {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -59,6 +67,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
|||||||
return _repository!.sendString(message);
|
return _repository!.sendString(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> sendBytes(List<int> bytes) {
|
||||||
|
_checkInitialized();
|
||||||
|
return _repository!.sendBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Map<String, dynamic>> get messageStream {
|
Stream<Map<String, dynamic>> get messageStream {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -71,6 +85,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
|||||||
return _repository!.rawMessageStream;
|
return _repository!.rawMessageStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> get binaryMessageStream {
|
||||||
|
_checkInitialized();
|
||||||
|
return _repository!.binaryMessageStream;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<SocketConnectionState> get connectionStateStream {
|
Stream<SocketConnectionState> get connectionStateStream {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -103,4 +123,3 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import '../../../networks_sdk.dart';
|
import '../../../networks_sdk.dart';
|
||||||
import 'networks_sdk_core.dart';
|
import 'networks_sdk_core.dart';
|
||||||
|
|
||||||
/// SDK API Implementation
|
/// [NetworksSdkApi] 的实现,透传给 Repository
|
||||||
class NetworksSdkApiImpl implements NetworksSdkApi {
|
class NetworksSdkApiImpl implements NetworksSdkApi {
|
||||||
final NetworksSdkCore _core;
|
final NetworksSdkCore _core;
|
||||||
|
|
||||||
@@ -14,6 +14,29 @@ class NetworksSdkApiImpl implements NetworksSdkApi {
|
|||||||
void initialize(ApiConfig apiConfig) => _core.repo.initialize(apiConfig);
|
void initialize(ApiConfig apiConfig) => _core.repo.initialize(apiConfig);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) => _core.repo.executeRequest(request);
|
Future<T?> executeRequest<T>(
|
||||||
|
ApiRequestable<T> request, {
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _core.repo.executeRequest(request, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> executeDownload({
|
||||||
|
required String url,
|
||||||
|
required String savePath,
|
||||||
|
OnDownloadProgress? onProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
bool resume = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) {
|
||||||
|
return _core.repo.executeDownload(
|
||||||
|
url: url,
|
||||||
|
savePath: savePath,
|
||||||
|
onProgress: onProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
resume: resume,
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||||
|
|
||||||
/// WebSocket 配置
|
/// WebSocket 配置
|
||||||
/// 非单例,由 App 层构造并注入到 SocketClient
|
/// 非单例,由 App 层构造并注入到 SocketClient
|
||||||
///
|
///
|
||||||
/// 与 [ApiConfig] 设计一致:SDK 不依赖 Flutter,
|
/// 与 [ApiConfig] 设计一致:SDK 不依赖 Flutter,
|
||||||
/// 网络检测、生命周期等业务逻辑通过回调注入。
|
/// 网络检测、生命周期等业务逻辑通过回调注入。
|
||||||
class SocketConfig {
|
class SocketConfig {
|
||||||
|
// ── 心跳 ──
|
||||||
|
|
||||||
/// 应用层心跳间隔(定时发送 "ping" 字符串)
|
/// 应用层心跳间隔(定时发送 "ping" 字符串)
|
||||||
final Duration heartbeatInterval;
|
final Duration heartbeatInterval;
|
||||||
|
|
||||||
@@ -13,10 +17,19 @@ class SocketConfig {
|
|||||||
/// Pong 超时(超过此时间未收到 pong 则判定连接断开)
|
/// Pong 超时(超过此时间未收到 pong 则判定连接断开)
|
||||||
final Duration pongTimeout;
|
final Duration pongTimeout;
|
||||||
|
|
||||||
|
// ── 连接 ──
|
||||||
|
|
||||||
/// 连接超时
|
/// 连接超时
|
||||||
final Duration connectTimeout;
|
final Duration connectTimeout;
|
||||||
|
|
||||||
|
/// 是否启用 WebSocket 压缩(permessage-deflate)
|
||||||
|
final bool enableCompression;
|
||||||
|
|
||||||
|
// ── 重连 ──
|
||||||
|
|
||||||
/// 最大重连次数(0 = 不重连)
|
/// 最大重连次数(0 = 不重连)
|
||||||
|
///
|
||||||
|
/// 当 [unlimitedReconnect] 为 true 时此字段无效。
|
||||||
final int maxReconnectAttempts;
|
final int maxReconnectAttempts;
|
||||||
|
|
||||||
/// 最大重连延迟(指数退避上限)
|
/// 最大重连延迟(指数退避上限)
|
||||||
@@ -25,22 +38,65 @@ class SocketConfig {
|
|||||||
/// 是否自动重连
|
/// 是否自动重连
|
||||||
final bool autoReconnect;
|
final bool autoReconnect;
|
||||||
|
|
||||||
|
/// 无限重连模式
|
||||||
|
///
|
||||||
|
/// IM 场景建议开启:连接断开后始终尝试重连,不受
|
||||||
|
/// [maxReconnectAttempts] 限制。退避延迟仍受
|
||||||
|
/// [maxReconnectDelay] 约束。
|
||||||
|
final bool unlimitedReconnect;
|
||||||
|
|
||||||
|
// ── 回调 ──
|
||||||
|
|
||||||
/// 日志输出回调(与 ApiConfig.onLog 同签名)
|
/// 日志输出回调(与 ApiConfig.onLog 同签名)
|
||||||
final void Function(String message, {String? tag})? onLog;
|
final OnLog? onLog;
|
||||||
|
|
||||||
/// 网络可用性查询(App 层注入,SDK 在重连前调用)
|
/// 网络可用性查询(App 层注入,SDK 在重连前调用)
|
||||||
/// 返回 true 表示网络可用,可以尝试重连
|
/// 返回 true 表示网络可用,可以尝试重连
|
||||||
final Future<bool> Function()? onCheckNetworkAvailable;
|
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
|
||||||
|
|
||||||
|
/// 重连前回调
|
||||||
|
///
|
||||||
|
/// 每次自动重连前调用(心跳超时、连接断开等触发的内部重连)。
|
||||||
|
/// App 层用于:
|
||||||
|
/// - 检查并刷新即将过期的 token(通过 [SocketClient.updateToken])
|
||||||
|
/// - 其他重连前准备工作
|
||||||
|
///
|
||||||
|
/// 回调完成后才发起实际连接。如果回调抛出异常,本次重连跳过,
|
||||||
|
/// 等下一轮退避定时器触发。
|
||||||
|
final Future<void> Function()? onBeforeReconnect;
|
||||||
|
|
||||||
|
// ── 加密回调(预留给 cipher_guard_sdk)──
|
||||||
|
|
||||||
|
/// 连接 URL 构建回调
|
||||||
|
///
|
||||||
|
/// 建立连接前调用,接收原始 URL 和 token,返回最终连接 URL 字符串。
|
||||||
|
/// null 时使用默认行为(URL 后追加 `?token=xxx`)。
|
||||||
|
///
|
||||||
|
/// App 层注入 cipher_guard_sdk 的加密逻辑:路径/token 加密、
|
||||||
|
/// 添加 `cipher=true` 参数等。
|
||||||
|
final OnBuildConnectUrl? onBuildConnectUrl;
|
||||||
|
|
||||||
|
/// 发送前加密回调,null 时不加密
|
||||||
|
final OnEncryptMessage? onEncryptMessage;
|
||||||
|
|
||||||
|
/// 收到后解密回调,null 时不解密
|
||||||
|
final OnDecryptMessage? onDecryptMessage;
|
||||||
|
|
||||||
SocketConfig({
|
SocketConfig({
|
||||||
this.heartbeatInterval = const Duration(seconds: 10),
|
this.heartbeatInterval = const Duration(seconds: 10),
|
||||||
this.pingInterval = const Duration(seconds: 5),
|
this.pingInterval = const Duration(seconds: 5),
|
||||||
this.pongTimeout = const Duration(seconds: 10),
|
this.pongTimeout = const Duration(seconds: 10),
|
||||||
this.connectTimeout = const Duration(seconds: 15),
|
this.connectTimeout = const Duration(seconds: 15),
|
||||||
|
this.enableCompression = false,
|
||||||
this.maxReconnectAttempts = 5,
|
this.maxReconnectAttempts = 5,
|
||||||
this.maxReconnectDelay = const Duration(seconds: 30),
|
this.maxReconnectDelay = const Duration(seconds: 30),
|
||||||
this.autoReconnect = true,
|
this.autoReconnect = true,
|
||||||
|
this.unlimitedReconnect = false,
|
||||||
this.onLog,
|
this.onLog,
|
||||||
this.onCheckNetworkAvailable,
|
this.onCheckNetworkAvailable,
|
||||||
|
this.onBeforeReconnect,
|
||||||
|
this.onBuildConnectUrl,
|
||||||
|
this.onEncryptMessage,
|
||||||
|
this.onDecryptMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.notification_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.notification_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/notification_sdk/android/build.gradle.kts
Normal file
71
packages/notification_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.notification_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.notification_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class NotificationSdkPlugin: NSObject, FlutterPlugin {
|
public class NotificationSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.protocol_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.protocol_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/protocol_sdk/android/build.gradle.kts
Normal file
71
packages/protocol_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.protocol_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.protocol_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class ProtocolSdkPlugin: NSObject, FlutterPlugin {
|
public class ProtocolSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.rtc_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.rtc_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/rtc_sdk/android/build.gradle.kts
Normal file
71
packages/rtc_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.rtc_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.rtc_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class RtcSdkPlugin: NSObject, FlutterPlugin {
|
public class RtcSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
group = "com.example.storage_sdk"
|
|
||||||
version = "1.0-SNAPSHOT"
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "2.2.20"
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle:8.11.1")
|
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: "com.android.library"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.example.storage_sdk"
|
|
||||||
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += "src/main/kotlin"
|
|
||||||
test.java.srcDirs += "src/test/kotlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
|
||||||
outputs.upToDateWhen {false}
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
packages/storage_sdk/android/build.gradle.kts
Normal file
71
packages/storage_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
group = "com.example.storage_sdk"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.storage_sdk"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Flutter
|
@preconcurrency import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class StorageSdkPlugin: NSObject, FlutterPlugin {
|
public class StorageSdkPlugin: NSObject, FlutterPlugin {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
|||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '6.2'
|
||||||
|
|
||||||
# If your plugin requires a privacy manifest, for example if it uses any
|
# If your plugin requires a privacy manifest, for example if it uses any
|
||||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||||
|
|||||||
38
pubspec.lock
38
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "92.0.0"
|
version: "92.0.0"
|
||||||
|
adaptive_number:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: adaptive_number
|
||||||
|
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -241,6 +249,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
dart_jsonwebtoken:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_jsonwebtoken
|
||||||
|
sha256: c6ecb3bb991c459b91c5adf9e871113dcb32bbe8fe7ca2c92723f88ffc1e0b7a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.2"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -297,6 +313,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.8"
|
version: "0.2.8"
|
||||||
|
ed25519_edwards:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ed25519_edwards
|
||||||
|
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
encrypt:
|
encrypt:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -471,14 +495,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
js:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: js
|
|
||||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.2"
|
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -720,13 +736,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
pointycastle:
|
pointycastle:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: pointycastle
|
name: pointycastle
|
||||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.1"
|
version: "4.0.0"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ workspace:
|
|||||||
- packages/storage_sdk
|
- packages/storage_sdk
|
||||||
- packages/im_log_sdk
|
- packages/im_log_sdk
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
# encrypt 5.0.3 限制 pointycastle ^3.6.2,但 dart_jsonwebtoken 需要 ^4.0.0。
|
||||||
|
# pointycastle 4.0.0 无破坏性 API 变更(主要是新增算法和泛型改进),
|
||||||
|
# encrypt 在 4.0.0 下运行无问题,强制升级解决冲突。
|
||||||
|
pointycastle: ^4.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
melos: ^7.0.0
|
melos: ^7.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,117 @@ with open(path, 'w') as f:
|
|||||||
f.write(content)
|
f.write(content)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
# ---- Step 3.5: 修正 podspec swift_version ----
|
||||||
|
# flutter create 默认生成 swift_version = '5.0',统一改为项目标准 6.2
|
||||||
|
for podspec in \
|
||||||
|
"$PKG_DIR/ios/${PKG_NAME}.podspec" \
|
||||||
|
"$PKG_DIR/macos/${PKG_NAME}.podspec"; do
|
||||||
|
if [[ -f "$podspec" ]]; then
|
||||||
|
sed -i '' "s/s.swift_version = '5.0'/s.swift_version = '6.2'/" "$podspec"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Flutter SDK 尚未完整标注 Swift 6 并发属性,用 @preconcurrency import 将
|
||||||
|
# FlutterMethodNotImplemented 等全局变量的并发警告降级,避免编译失败。
|
||||||
|
# 注意:不在类上加 @MainActor,否则与 FlutterPlugin 协议的 nonisolated 要求冲突,
|
||||||
|
# 导致 ConformanceIsolation 编译错误。
|
||||||
|
for swift_file in \
|
||||||
|
"$PKG_DIR/ios/Classes/${PASCAL_NAME}SdkPlugin.swift" \
|
||||||
|
"$PKG_DIR/macos/Classes/${PASCAL_NAME}SdkPlugin.swift"; do
|
||||||
|
if [[ -f "$swift_file" ]]; then
|
||||||
|
sed -i '' \
|
||||||
|
's/^import Flutter$/@preconcurrency import Flutter/' \
|
||||||
|
"$swift_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- Step 3.6: 替换 Android build.gradle → build.gradle.kts(Kotlin DSL + Compose) ----
|
||||||
|
# flutter create 默认生成 Groovy build.gradle,统一替换为 Kotlin DSL
|
||||||
|
ANDROID_DIR="$PKG_DIR/android"
|
||||||
|
[[ -f "$ANDROID_DIR/build.gradle" ]] && rm "$ANDROID_DIR/build.gradle"
|
||||||
|
|
||||||
|
cat > "$ANDROID_DIR/build.gradle.kts" << GRADLE_EOF
|
||||||
|
group = "com.example.${PKG_NAME}"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:\$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
// 若该 SDK 需要 Compose,取消注释下面两行,并在 dependencies 中加 compose runtime
|
||||||
|
// id("org.jetbrains.kotlin.plugin.compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.${PKG_NAME}"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||||
|
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若该 SDK 需要 Compose,取消注释
|
||||||
|
// buildFeatures { compose = true }
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// 若该 SDK 需要 Compose,取消注释以下两行并按需引入具体组件:
|
||||||
|
// val composeBom = platform("androidx.compose:compose-bom:2025.05.01")
|
||||||
|
// implementation(composeBom)
|
||||||
|
// implementation("androidx.compose.runtime:runtime")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
|
GRADLE_EOF
|
||||||
|
|
||||||
# ---- Step 4: IDE 配置 ----
|
# ---- Step 4: IDE 配置 ----
|
||||||
echo "[4/5] Updating IDE config..."
|
echo "[4/5] Updating IDE config..."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user