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:
Cody
2026-03-08 20:47:28 +08:00
88 changed files with 5695 additions and 593 deletions

View File

@@ -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&lt;ApiRequest&gt; {
@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 &amp;&amp; !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&lt;$responseTypeName&gt; {
@override String get path =&gt; '$path';
@override HttpMethod get method =&gt; HttpMethod.$methodName;
@override ApiRequestType get requestType =&gt; ApiRequestType.$requestTypeName;
@override bool get includeToken =&gt; $includeToken;
@override
Map&lt;String, dynamic&gt; toJson() =&gt; $toJsonBody;
@override
Map&lt;String, dynamic&gt;? get parameters {
registerResponse&lt;$responseTypeName&gt;($responseTypeName.fromJson);
return super.parameters;
@@ -2484,16 +2472,28 @@ mixin _\$${className}Api on ApiRequestable&lt;$responseTypeName&gt; {
}
''';
}
/// 读取类的声明字段,生成 Map 字面量
/// 支持 @JsonKey(name: '...') 重命名
String _buildToJsonBody(ClassElement element, String className) {
final fields = element.fields.where((f) =&gt; !f.isStatic &amp;&amp; !f.isSynthetic);
// =&gt; {'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&lt;T&gt; with _$XxxApi</code></p>
<p>所有示例遵循同一模式:<code>@ApiRequest</code> + <code>extends ApiRequestable&lt;T&gt; 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&lt;SendMessageData&gt;
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&lt;String, dynamic&gt; toJson() =&gt; _$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&lt;ProfileData&gt;
with _$GetProfileRequestApi {
GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$GetProfileRequestToJson(this);
GetProfileRequest(); // 无参数 — toJson 自动生成空 map
}
</code></pre>
@@ -2622,8 +2616,8 @@ class UploadFileRequest extends ApiRequestable&lt;UploadResult&gt;
<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&lt;T&gt;</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>Viewview/只负责渲染和用户交互ViewModelpresentation/持有状态并处理业务逻辑Modelmodel/ + 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>ViewWidget层对业务数据严格只读。所有逻辑导航、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&lt;UploadResult&gt;
@override
Object? get uploadData =&gt; 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) =&gt; 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>主线程零阻塞,不卡 UI2048-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>