Merge remote-tracking branch 'origin/dev' into cody/netwrok_SDK
# Conflicts: # apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart # apps/im_app/lib/features/login/presentation/login_view_model.dart 修复逻辑漏洞,性能优化
This commit is contained in:
@@ -895,7 +895,7 @@ flowchart TD
|
||||
│ │ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口
|
||||
│ │ ├── dto/
|
||||
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
|
||||
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||||
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析
|
||||
│ │ └── repositories/
|
||||
│ │ ├── networks_sdk_repository_impl.dart
|
||||
│ │ └── networks_messaging_repository_impl.dart
|
||||
@@ -2299,8 +2299,8 @@ class LoginData {
|
||||
User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
// @ApiRequest 自动生成 path / method / requestType / includeToken / fromJson 注册
|
||||
// ── Request(零样板:只需 @ApiRequest,无需 @JsonSerializable / fromJson / toJson)──
|
||||
// @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册
|
||||
|
||||
@ApiRequest(
|
||||
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
|
||||
@@ -2308,15 +2308,12 @@ class LoginData {
|
||||
responseType: LoginData,
|
||||
requestType: ApiRequestType.login,
|
||||
)
|
||||
@JsonSerializable()
|
||||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
LoginRequest({required this.email, required this.password});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||
// 完毕!toJson 由 mixin 从类字段自动生成,fromJson 不需要(Request 永远手动构造)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
@@ -2353,17 +2350,17 @@ final user = loginData?.toEntity(); // DTO → Domain Entity
|
||||
<td>中</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>@ApiRequest + @JsonSerializable</strong></td>
|
||||
<td>字段 + 构造函数 + @ApiRequest + @JsonSerializable</td>
|
||||
<td>path / method / requestType / includeToken / toJson / fromJson / fromJson 注册</td>
|
||||
<td><strong>低</strong></td>
|
||||
<td><strong>@ApiRequest(当前方案)</strong></td>
|
||||
<td>字段 + 构造函数 + @ApiRequest</td>
|
||||
<td>path / method / requestType / includeToken / toJson / fromJson 注册</td>
|
||||
<td><strong>极低</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>核心优势</strong>:</p>
|
||||
<ul>
|
||||
<li><strong>注解驱动</strong>:<code>@ApiRequest</code> 自动生成 mixin,<code>@JsonSerializable</code> 自动生成 toJson/fromJson</li>
|
||||
<li><strong>注解驱动</strong>:<code>@ApiRequest</code> 一个注解自动生成 mixin(含 toJson),无需 <code>@JsonSerializable</code></li>
|
||||
<li><strong>自动注册</strong>:fromJson 在首次请求时自动注册到全局注册表,无需手动 <code>registerApiResponses()</code></li>
|
||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
||||
<li><strong>傻瓜式使用</strong>:使用者只需关注业务字段和注解配置</li>
|
||||
@@ -2447,36 +2444,27 @@ class ApiRequest {
|
||||
|
||||
<p>生成 <strong>mixin</strong>(非 extension),因为 mixin 可以 override 基类方法、调用 <code>super</code>,并在 <code>parameters</code> getter 中自动注册 fromJson。</p>
|
||||
|
||||
<p><strong>toJson 生成机制</strong>:生成器读取类的<strong>声明字段</strong>(非继承),直接在 mixin 中生成 Map 字面量。不依赖 <code>@JsonSerializable</code>,避免了继承属性被序列化导致的递归问题。支持 <code>@JsonKey(name: '...')</code> 字段重命名和 <code>@JsonKey(includeToJson: false)</code> 跳过字段。</p>
|
||||
|
||||
<pre><code class="language-dart">/// API 请求代码生成器
|
||||
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
|
||||
@override
|
||||
String generateForAnnotatedElement(
|
||||
Element element,
|
||||
ConstantReader annotation,
|
||||
BuildStep buildStep,
|
||||
) {
|
||||
String generateForAnnotatedElement(element, annotation, buildStep) {
|
||||
final className = element.name;
|
||||
final path = annotation.read('path').stringValue;
|
||||
final methodName = _readEnumName(annotation.read('method').objectValue, 'post');
|
||||
final responseType = annotation.read('responseType').typeValue;
|
||||
final responseTypeName = responseType.getDisplayString();
|
||||
final requestTypeName = _readEnumName(annotation.read('requestType').objectValue, 'request');
|
||||
// ... 读取 path / method / responseType / requestType / includeToken ...
|
||||
|
||||
// includeToken:默认 login → false,其余 → true
|
||||
final includeTokenReader = annotation.peek('includeToken');
|
||||
final includeToken = (includeTokenReader != null && !includeTokenReader.isNull)
|
||||
? includeTokenReader.boolValue
|
||||
: requestTypeName != 'login';
|
||||
// 从类的声明字段生成 toJson(),只序列化自身字段,不含继承属性
|
||||
final toJsonBody = _buildToJsonBody(element, className);
|
||||
|
||||
// 生成 mixin,使用侧只需 `with _$XxxApi`
|
||||
return '''
|
||||
/// Generated by @ApiRequest for [$className]
|
||||
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
||||
@override String get path => '$path';
|
||||
@override HttpMethod get method => HttpMethod.$methodName;
|
||||
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
|
||||
@override bool get includeToken => $includeToken;
|
||||
@override
|
||||
Map<String, dynamic> toJson() => $toJsonBody;
|
||||
@override
|
||||
Map<String, dynamic>? get parameters {
|
||||
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
||||
return super.parameters;
|
||||
@@ -2484,16 +2472,28 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
||||
/// 读取类的声明字段,生成 Map 字面量
|
||||
/// 支持 @JsonKey(name: '...') 重命名
|
||||
String _buildToJsonBody(ClassElement element, String className) {
|
||||
final fields = element.fields.where((f) => !f.isStatic && !f.isSynthetic);
|
||||
// => {'email': (this as LoginRequest).email, 'password': ...}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p><strong>关键设计</strong>:<code>parameters</code> getter 在首次请求时自动调用 <code>registerResponse</code>,将 <code>fromJson</code> 注册到全局注册表。无需手动注册,也无需 <code>registerApiResponses()</code> 启动函数。</p>
|
||||
<p><strong>关键设计</strong>:</p>
|
||||
<ul>
|
||||
<li><code>toJson()</code> 只序列化类自身声明的字段,不含 <code>ApiRequestable</code> 的继承属性(path / method / parameters 等),避免递归</li>
|
||||
<li><code>parameters</code> getter 在首次请求时自动调用 <code>registerResponse</code>,将 Response 的 <code>fromJson</code> 注册到全局注册表</li>
|
||||
<li>Upload 等特殊请求在类中 override <code>toJson()</code>,类的 override 优先于 mixin</li>
|
||||
</ul>
|
||||
|
||||
<h6>4.3 build.yaml 配置</h6>
|
||||
|
||||
<p><strong>文件:<code>packages/networks_sdk/build.yaml</code></strong></p>
|
||||
|
||||
<p>使用 <code>SharedPartBuilder</code>,与 <code>@JsonSerializable</code> 共享同一个 <code>.g.dart</code> 文件,无需额外 part 指令。</p>
|
||||
<p>使用 <code>SharedPartBuilder</code>,与 <code>@JsonSerializable</code>(Response DTO 用)共享同一个 <code>.g.dart</code> 文件,无需额外 part 指令。</p>
|
||||
|
||||
<pre><code class="language-yaml">builders:
|
||||
api_request:
|
||||
@@ -2522,12 +2522,12 @@ melos run gen
|
||||
|
||||
<h6>4.5 更多使用示例</h6>
|
||||
|
||||
<p>所有示例遵循同一模式:<code>@ApiRequest</code> + <code>@JsonSerializable</code> + <code>extends ApiRequestable<T> with _$XxxApi</code>。</p>
|
||||
<p>所有示例遵循同一模式:<code>@ApiRequest</code> + <code>extends ApiRequestable<T> with _$XxxApi</code>。Request 类无需 <code>@JsonSerializable</code>。</p>
|
||||
|
||||
<p><strong>发送消息请求(POST):</strong></p>
|
||||
<p><strong>发送消息请求(POST + @JsonKey 字段重命名):</strong></p>
|
||||
<pre><code class="language-dart">// data/remote/send_message_request.dart
|
||||
|
||||
// ── Response DTO ──
|
||||
// ── Response DTO(仍用 @JsonSerializable)──
|
||||
@JsonSerializable()
|
||||
class SendMessageData {
|
||||
@JsonKey(name: 'message_id')
|
||||
@@ -2539,25 +2539,23 @@ class SendMessageData {
|
||||
_$SendMessageDataFromJson(json);
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
// ── Request(零样板)──
|
||||
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
|
||||
@JsonSerializable()
|
||||
class SendMessageRequest extends ApiRequestable<SendMessageData>
|
||||
with _$SendMessageRequestApi {
|
||||
@JsonKey(name: 'chat_id')
|
||||
@JsonKey(name: 'chat_id') // 生成器会读取,JSON 键名为 'chat_id'
|
||||
final String chatId;
|
||||
final String content;
|
||||
|
||||
SendMessageRequest({required this.chatId, required this.content});
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
|
||||
// toJson 自动生成:{'chat_id': chatId, 'content': content}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p><strong>获取用户资料(GET,靠 token 标识当前用户,无需传参):</strong></p>
|
||||
<pre><code class="language-dart">// data/remote/get_profile_request.dart
|
||||
|
||||
@JsonSerializable()
|
||||
@JsonSerializable(createToJson: false) // 只需反序列化
|
||||
class ProfileData {
|
||||
@JsonKey(name: 'user_id')
|
||||
final String userId;
|
||||
@@ -2573,13 +2571,9 @@ class ProfileData {
|
||||
}
|
||||
|
||||
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
|
||||
@JsonSerializable()
|
||||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||||
with _$GetProfileRequestApi {
|
||||
GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
||||
GetProfileRequest(); // 无参数 — toJson 自动生成空 map
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
@@ -2622,8 +2616,8 @@ class UploadFileRequest extends ApiRequestable<UploadResult>
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
|
||||
<p><strong>核心价值</strong></p>
|
||||
<ul style="margin-bottom: 0;">
|
||||
<li><strong>极简使用</strong>:字段 + 构造函数 + <code>@ApiRequest</code> + <code>@JsonSerializable</code></li>
|
||||
<li><strong>零维护</strong>:path / method / requestType / includeToken / fromJson 注册 全部自动生成</li>
|
||||
<li><strong>极简使用</strong>:字段 + 构造函数 + <code>@ApiRequest</code>(Request 无需 <code>@JsonSerializable</code>、无需 <code>fromJson</code>、无需手写 <code>toJson</code>)</li>
|
||||
<li><strong>零维护</strong>:path / method / requestType / includeToken / toJson / fromJson 注册 全部自动生成</li>
|
||||
<li><strong>类型安全</strong>:泛型 <code>ApiRequestable<T></code> + <code>responseType</code> 编译期检查</li>
|
||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
||||
</ul>
|
||||
@@ -3211,6 +3205,7 @@ flowchart LR
|
||||
<p><strong>两大核心逻辑</strong>:</p>
|
||||
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
|
||||
<p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code> → <code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。</p>
|
||||
<p>3. <strong>Widget 纯展示原则</strong>:View(Widget)层对业务数据严格只读。所有逻辑(导航、CRUD、状态变更、条件判断)必须在 ViewModel 中完成,View 只调用 ViewModel 方法并渲染返回的 State。包括 demo/测试页面也不例外。</p>
|
||||
</blockquote>
|
||||
|
||||
<hr>
|
||||
@@ -5698,7 +5693,7 @@ class MessageLocalDataSource {
|
||||
<ul>
|
||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
||||
<li><strong>Repository 直接调 NetworksSdkApi</strong>:无需 RemoteDataSource 中间层</li>
|
||||
<li><strong>@ApiRequest 注解 + 代码生成</strong>:自动实现 path / method / fromJson 注册</li>
|
||||
<li><strong>@ApiRequest 注解 + 代码生成</strong>:自动实现 path / method / toJson / fromJson 注册(Request 无需 @JsonSerializable)</li>
|
||||
</ul>
|
||||
|
||||
<pre><code>// 示例:Repository 直接调用 Request
|
||||
@@ -5907,7 +5902,7 @@ flowchart LR
|
||||
│ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口(executeRequest / executeDownload)
|
||||
│ ├── dto/
|
||||
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
|
||||
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||||
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析
|
||||
│ └── repositories/
|
||||
│ ├── networks_sdk_repository_impl.dart
|
||||
│ └── networks_messaging_repository_impl.dart
|
||||
@@ -5954,7 +5949,7 @@ flowchart LR
|
||||
<tr><td>WebSocket 连接</td><td>SocketClient 内部管理(连接/心跳/重连)</td><td>调 connect/disconnect/send</td></tr>
|
||||
<tr><td>WebSocket 心跳</td><td>双层心跳自动管理(底层 ping 5s + 应用层 10s)</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)。本项目 disconnectInBackground=false,所有平台后台保活、心跳不停</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 包装 NetworksSdkApi / SocketClient</td></tr>
|
||||
</tbody>
|
||||
@@ -6529,7 +6524,7 @@ class UploadFileRequest extends ApiRequestable<UploadResult>
|
||||
@override
|
||||
Object? get uploadData => data; // Uint8List 直接作为 body
|
||||
|
||||
/// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 信封
|
||||
/// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 响应格式
|
||||
/// 必须 override decodeResponse
|
||||
@override
|
||||
S3UploadResponse? decodeResponse(Response response) {
|
||||
@@ -6798,12 +6793,54 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));
|
||||
<p>端对端加密 SDK,同时处理 Dart 侧加解密和 Native 侧密钥同步(iOS App Group 用于推送通知解密):</p>
|
||||
<ul>
|
||||
<li><code>cipher_guard_sdk_api.dart</code>:公开 API 接口(Facade)</li>
|
||||
<li><code>encryption_flutter_service.dart</code>:RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt)</li>
|
||||
<li><code>encryption_flutter_service.dart</code>:RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt),含性能优化</li>
|
||||
<li><code>encryption_method_channel.dart</code>:Native 密钥同步通道(iOS App Group 共享密钥供 Notification Extension 解密)</li>
|
||||
<li>Domain 实体:<code>RsaKeyPair</code> / <code>SessionKey</code> / <code>EncryptedMessage</code> / <code>ChatEncryptionKey</code></li>
|
||||
<li><code>android/</code> + <code>ios/</code>:Plugin 注册入口,原生侧实现密钥写入 App Group</li>
|
||||
</ul>
|
||||
|
||||
<h5>加解密性能优化</h5>
|
||||
|
||||
<p><code>encryption_flutter_service.dart</code> 针对 IM 高频加解密场景做了四项优化:</p>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>优化项</th>
|
||||
<th>方案</th>
|
||||
<th>效果</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>RSA 密钥生成异步化</strong></td>
|
||||
<td><code>generateRsaKeyPairAsync</code> 使用 <code>Isolate.run()</code> 在独立线程生成</td>
|
||||
<td>主线程零阻塞,不卡 UI(2048-bit 约 200-500ms)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>派生密钥 LRU 缓存</strong></td>
|
||||
<td><code>_derivedKeyCache</code>(Map,上限 64 条),缓存键 = <code>sessionKey:round:mode</code>,满时淘汰最早条目</td>
|
||||
<td>同一 round 的加解密只算一次 KDF,后续直接命中缓存</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Random.secure() 复用</strong></td>
|
||||
<td>静态 <code>_secureRandom</code> 单例,所有 IV / 随机数生成共用</td>
|
||||
<td>避免每次 <code>Random.secure()</code> 构造开销</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>KDF 双模式</strong></td>
|
||||
<td><code>KdfMode.md5</code>(默认,兼容既有数据)和 <code>KdfMode.pbkdf2</code>(PBKDF2-HMAC-SHA256,可配迭代次数)</td>
|
||||
<td>默认快速兼容,可选安全增强(防暴力破解)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>构造时可配置 KDF 模式和 PBKDF2 迭代次数:</p>
|
||||
<pre><code>EncryptionFlutterService(
|
||||
kdfMode: KdfMode.md5, // 默认,兼容既有数据
|
||||
pbkdf2Iterations: 10000, // PBKDF2 模式下的迭代次数
|
||||
)</code></pre>
|
||||
|
||||
<p><code>clearDerivedKeyCache()</code> 可在 session key 轮换时手动清空缓存。</p>
|
||||
|
||||
<h3 id="7-4-l10n">7.4 多语言国际化(packages/l10n_sdk/)</h3>
|
||||
|
||||
<p>已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。</p>
|
||||
|
||||
Reference in New Issue
Block a user