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 # 统一执行入口
|
│ │ │ └── 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 } 响应包装解析
|
||||||
│ │ └── repositories/
|
│ │ └── repositories/
|
||||||
│ │ ├── networks_sdk_repository_impl.dart
|
│ │ ├── networks_sdk_repository_impl.dart
|
||||||
│ │ └── networks_messaging_repository_impl.dart
|
│ │ └── networks_messaging_repository_impl.dart
|
||||||
@@ -2299,8 +2299,8 @@ class LoginData {
|
|||||||
User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
|
User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Request ──
|
// ── Request(零样板:只需 @ApiRequest,无需 @JsonSerializable / fromJson / toJson)──
|
||||||
// @ApiRequest 自动生成 path / method / requestType / includeToken / fromJson 注册
|
// @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册
|
||||||
|
|
||||||
@ApiRequest(
|
@ApiRequest(
|
||||||
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
|
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
|
||||||
@@ -2308,15 +2308,12 @@ class LoginData {
|
|||||||
responseType: LoginData,
|
responseType: LoginData,
|
||||||
requestType: ApiRequestType.login,
|
requestType: ApiRequestType.login,
|
||||||
)
|
)
|
||||||
@JsonSerializable()
|
|
||||||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||||
final String email;
|
final String email;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
LoginRequest({required this.email, required this.password});
|
LoginRequest({required this.email, required this.password});
|
||||||
|
// 完毕!toJson 由 mixin 从类字段自动生成,fromJson 不需要(Request 永远手动构造)
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
@@ -2353,17 +2350,17 @@ final user = loginData?.toEntity(); // DTO → Domain Entity
|
|||||||
<td>中</td>
|
<td>中</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>@ApiRequest + @JsonSerializable</strong></td>
|
<td><strong>@ApiRequest(当前方案)</strong></td>
|
||||||
<td>字段 + 构造函数 + @ApiRequest + @JsonSerializable</td>
|
<td>字段 + 构造函数 + @ApiRequest</td>
|
||||||
<td>path / method / requestType / includeToken / toJson / fromJson / fromJson 注册</td>
|
<td>path / method / requestType / includeToken / toJson / fromJson 注册</td>
|
||||||
<td><strong>低</strong></td>
|
<td><strong>极低</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><strong>核心优势</strong>:</p>
|
<p><strong>核心优势</strong>:</p>
|
||||||
<ul>
|
<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>:fromJson 在首次请求时自动注册到全局注册表,无需手动 <code>registerApiResponses()</code></li>
|
||||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
||||||
<li><strong>傻瓜式使用</strong>:使用者只需关注业务字段和注解配置</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>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 请求代码生成器
|
<pre><code class="language-dart">/// API 请求代码生成器
|
||||||
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
|
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
|
||||||
@override
|
@override
|
||||||
String generateForAnnotatedElement(
|
String generateForAnnotatedElement(element, annotation, buildStep) {
|
||||||
Element element,
|
|
||||||
ConstantReader annotation,
|
|
||||||
BuildStep buildStep,
|
|
||||||
) {
|
|
||||||
final className = element.name;
|
final className = element.name;
|
||||||
final path = annotation.read('path').stringValue;
|
// ... 读取 path / method / responseType / requestType / includeToken ...
|
||||||
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');
|
|
||||||
|
|
||||||
// includeToken:默认 login → false,其余 → true
|
// 从类的声明字段生成 toJson(),只序列化自身字段,不含继承属性
|
||||||
final includeTokenReader = annotation.peek('includeToken');
|
final toJsonBody = _buildToJsonBody(element, className);
|
||||||
final includeToken = (includeTokenReader != null && !includeTokenReader.isNull)
|
|
||||||
? includeTokenReader.boolValue
|
|
||||||
: requestTypeName != 'login';
|
|
||||||
|
|
||||||
// 生成 mixin,使用侧只需 `with _$XxxApi`
|
|
||||||
return '''
|
return '''
|
||||||
/// Generated by @ApiRequest for [$className]
|
|
||||||
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
||||||
@override String get path => '$path';
|
@override String get path => '$path';
|
||||||
@override HttpMethod get method => HttpMethod.$methodName;
|
@override HttpMethod get method => HttpMethod.$methodName;
|
||||||
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
|
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
|
||||||
@override bool get includeToken => $includeToken;
|
@override bool get includeToken => $includeToken;
|
||||||
@override
|
@override
|
||||||
|
Map<String, dynamic> toJson() => $toJsonBody;
|
||||||
|
@override
|
||||||
Map<String, dynamic>? get parameters {
|
Map<String, dynamic>? get parameters {
|
||||||
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
||||||
return super.parameters;
|
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>
|
</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>
|
<h6>4.3 build.yaml 配置</h6>
|
||||||
|
|
||||||
<p><strong>文件:<code>packages/networks_sdk/build.yaml</code></strong></p>
|
<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:
|
<pre><code class="language-yaml">builders:
|
||||||
api_request:
|
api_request:
|
||||||
@@ -2522,12 +2522,12 @@ melos run gen
|
|||||||
|
|
||||||
<h6>4.5 更多使用示例</h6>
|
<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
|
<pre><code class="language-dart">// data/remote/send_message_request.dart
|
||||||
|
|
||||||
// ── Response DTO ──
|
// ── Response DTO(仍用 @JsonSerializable)──
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class SendMessageData {
|
class SendMessageData {
|
||||||
@JsonKey(name: 'message_id')
|
@JsonKey(name: 'message_id')
|
||||||
@@ -2539,25 +2539,23 @@ class SendMessageData {
|
|||||||
_$SendMessageDataFromJson(json);
|
_$SendMessageDataFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Request ──
|
// ── Request(零样板)──
|
||||||
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
|
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
|
||||||
@JsonSerializable()
|
|
||||||
class SendMessageRequest extends ApiRequestable<SendMessageData>
|
class SendMessageRequest extends ApiRequestable<SendMessageData>
|
||||||
with _$SendMessageRequestApi {
|
with _$SendMessageRequestApi {
|
||||||
@JsonKey(name: 'chat_id')
|
@JsonKey(name: 'chat_id') // 生成器会读取,JSON 键名为 'chat_id'
|
||||||
final String chatId;
|
final String chatId;
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
SendMessageRequest({required this.chatId, required this.content});
|
SendMessageRequest({required this.chatId, required this.content});
|
||||||
@override
|
// toJson 自动生成:{'chat_id': chatId, 'content': content}
|
||||||
Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
|
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<p><strong>获取用户资料(GET,靠 token 标识当前用户,无需传参):</strong></p>
|
<p><strong>获取用户资料(GET,靠 token 标识当前用户,无需传参):</strong></p>
|
||||||
<pre><code class="language-dart">// data/remote/get_profile_request.dart
|
<pre><code class="language-dart">// data/remote/get_profile_request.dart
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable(createToJson: false) // 只需反序列化
|
||||||
class ProfileData {
|
class ProfileData {
|
||||||
@JsonKey(name: 'user_id')
|
@JsonKey(name: 'user_id')
|
||||||
final String userId;
|
final String userId;
|
||||||
@@ -2573,13 +2571,9 @@ class ProfileData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
|
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
|
||||||
@JsonSerializable()
|
|
||||||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||||||
with _$GetProfileRequestApi {
|
with _$GetProfileRequestApi {
|
||||||
GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
|
GetProfileRequest(); // 无参数 — toJson 自动生成空 map
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
|
||||||
}
|
}
|
||||||
</code></pre>
|
</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;">
|
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
|
||||||
<p><strong>核心价值</strong></p>
|
<p><strong>核心价值</strong></p>
|
||||||
<ul style="margin-bottom: 0;">
|
<ul style="margin-bottom: 0;">
|
||||||
<li><strong>极简使用</strong>:字段 + 构造函数 + <code>@ApiRequest</code> + <code>@JsonSerializable</code></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 / fromJson 注册 全部自动生成</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>:泛型 <code>ApiRequestable<T></code> + <code>responseType</code> 编译期检查</li>
|
||||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 放在同一文件,打开即看全貌</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -3211,6 +3205,7 @@ flowchart LR
|
|||||||
<p><strong>两大核心逻辑</strong>:</p>
|
<p><strong>两大核心逻辑</strong>:</p>
|
||||||
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</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>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>
|
</blockquote>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@@ -5698,7 +5693,7 @@ class MessageLocalDataSource {
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
<li><strong>一个端点 = 一个文件</strong>:Response DTO + Request 类放在同一文件中</li>
|
||||||
<li><strong>Repository 直接调 NetworksSdkApi</strong>:无需 RemoteDataSource 中间层</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>
|
</ul>
|
||||||
|
|
||||||
<pre><code>// 示例:Repository 直接调用 Request
|
<pre><code>// 示例:Repository 直接调用 Request
|
||||||
@@ -5907,7 +5902,7 @@ flowchart LR
|
|||||||
│ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口(executeRequest / executeDownload)
|
│ │ └── 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 } 响应包装解析
|
||||||
│ └── repositories/
|
│ └── repositories/
|
||||||
│ ├── networks_sdk_repository_impl.dart
|
│ ├── networks_sdk_repository_impl.dart
|
||||||
│ └── networks_messaging_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>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>双层心跳自动管理(底层 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>指数退避自动重连(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>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>
|
<tr><td>Riverpod</td><td>无依赖</td><td>Provider 包装 NetworksSdkApi / SocketClient</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -6529,7 +6524,7 @@ class UploadFileRequest extends ApiRequestable<UploadResult>
|
|||||||
@override
|
@override
|
||||||
Object? get uploadData => data; // Uint8List 直接作为 body
|
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 decodeResponse
|
||||||
@override
|
@override
|
||||||
S3UploadResponse? decodeResponse(Response response) {
|
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>
|
<p>端对端加密 SDK,同时处理 Dart 侧加解密和 Native 侧密钥同步(iOS App Group 用于推送通知解密):</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>cipher_guard_sdk_api.dart</code>:公开 API 接口(Facade)</li>
|
<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><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>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>
|
<li><code>android/</code> + <code>ios/</code>:Plugin 注册入口,原生侧实现密钥写入 App Group</li>
|
||||||
</ul>
|
</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>
|
<h3 id="7-4-l10n">7.4 多语言国际化(packages/l10n_sdk/)</h3>
|
||||||
|
|
||||||
<p>已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。</p>
|
<p>已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。</p>
|
||||||
|
|||||||
31
apps/im_app/assets/loginData.json
Normal file
31
apps/im_app/assets/loginData.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": {
|
||||||
|
"account_id": "1713925030yFMUBu",
|
||||||
|
"profile": {
|
||||||
|
"uid": 2137067,
|
||||||
|
"uuid": "1713925030yFMUBu",
|
||||||
|
"last_online": 1772819822,
|
||||||
|
"profile_pic": "Image/7e/f5/7ef5b60dd83a34a74c164a21fbd1f098/7ef5b60dd83a34a74c164a21fbd1f098.jpg",
|
||||||
|
"profile_pic_gaussian": "",
|
||||||
|
"nickname": "Happi(哈比)",
|
||||||
|
"contact": "86552205",
|
||||||
|
"country_code": "+65",
|
||||||
|
"email": "happi@winwayinfo.com",
|
||||||
|
"recovery_email": "",
|
||||||
|
"username": "happi",
|
||||||
|
"bio": "",
|
||||||
|
"relationship": 2,
|
||||||
|
"user_alias": null,
|
||||||
|
"channel_id": 1,
|
||||||
|
"channel_group_id": 1,
|
||||||
|
"hint": "1111"
|
||||||
|
},
|
||||||
|
"nonce": "",
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjIxMzcwNjcsInVkaWQiOjI2NjI2MzcsInNpZCI6IjE3NzI4NjU0NDc0Z1c2VWc9PSIsImlzcyI6ImFiYy5jb20iLCJhdWQiOlsidXNlciJdLCJleHAiOjE3NzQxNjE0NDcsIm5iZiI6MTc3Mjg2NTQ0NywiaWF0IjoxNzcyODY1NDQ3fQ.gUL6hyKgyPP8Tw9y7kRSq-ndNKfV9uGFhU4YKiQzg0I",
|
||||||
|
"refresh_token": "ps0FF3XayvnJB_P8Cnfu7w-uD781b1-vfmUjbrONZxI=",
|
||||||
|
"device_id": "SP1A.210812",
|
||||||
|
"login_data": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
|||||||
onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调
|
onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调
|
||||||
onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
|
onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
|
||||||
onTransformResponse:
|
onTransformResponse:
|
||||||
null, // TODO: 如后端信封结构非标准,在此归一化为 { code, data, message }
|
null, // TODO: 如后端响应格式非标准,在此归一化为 { code, data, message }
|
||||||
onGetTokenExpiry: parseJwtExpiry,
|
onGetTokenExpiry: parseJwtExpiry,
|
||||||
maxRetries: AppConstants.maxRetries,
|
maxRetries: AppConstants.maxRetries,
|
||||||
retryBaseDelay: AppConstants.retryBaseDelay,
|
retryBaseDelay: AppConstants.retryBaseDelay,
|
||||||
@@ -214,6 +214,7 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
|
|||||||
final manager = SocketManager(
|
final manager = SocketManager(
|
||||||
client: client,
|
client: client,
|
||||||
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
||||||
|
disconnectInBackground: false, // 所有平台后台保活,心跳不停、连接不断
|
||||||
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
|
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
|
||||||
onBeforeReconnect: () async {
|
onBeforeReconnect: () async {
|
||||||
// 重连前检查 token 是否即将过期,是则主动刷新
|
// 重连前检查 token 是否即将过期,是则主动刷新
|
||||||
@@ -327,9 +328,11 @@ String _buildWsUrl(String httpBaseUrl) {
|
|||||||
//
|
//
|
||||||
// WidgetsBindingObserver(App 层 app.dart)
|
// WidgetsBindingObserver(App 层 app.dart)
|
||||||
// → SocketManager.onEnterBackground()
|
// → SocketManager.onEnterBackground()
|
||||||
// disconnectInBackground=true → disconnect(默认,移动端省电)
|
// disconnectInBackground=false → 完全保活,心跳不停(本项目默认)
|
||||||
// disconnectInBackground=false → 完全保活,不断连不暂停心跳(桌面端)
|
// disconnectInBackground=true → disconnect + 暂停心跳(省电模式)
|
||||||
// → SocketManager.onEnterForeground() → onBeforeReconnect → reconnect
|
// → SocketManager.onEnterForeground()
|
||||||
|
// 保活模式 → 检查连接健康,异常则重连
|
||||||
|
// 断连模式 → onBeforeReconnect → reconnect
|
||||||
//
|
//
|
||||||
// Token 刷新 → WebSocket 同步链路:
|
// Token 刷新 → WebSocket 同步链路:
|
||||||
//
|
//
|
||||||
@@ -524,5 +527,5 @@ String _buildWsUrl(String httpBaseUrl) {
|
|||||||
// Upload B: 二进制上传到 S3 presigned URL
|
// Upload B: 二进制上传到 S3 presigned URL
|
||||||
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
|
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
|
||||||
// @override Object? get uploadData => bytes; // Uint8List
|
// @override Object? get uploadData => bytes; // Uint8List
|
||||||
// @override decodeResponse(response) { ... } // S3 不走标准信封
|
// @override decodeResponse(response) { ... } // S3 不走标准响应格式
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ typedef MessageTransformer =
|
|||||||
///
|
///
|
||||||
/// 在 SocketClient(SDK 底层能力)之上封装:
|
/// 在 SocketClient(SDK 底层能力)之上封装:
|
||||||
/// - 连接/断连生命周期(登录连接、登出断连)
|
/// - 连接/断连生命周期(登录连接、登出断连)
|
||||||
/// - 前后台生命周期(后台断连省电、前台自动重连)
|
/// - 前后台生命周期(两种模式:后台断连 / 后台保活)
|
||||||
/// - 网络状态响应(断网断连、恢复网络立即重连)
|
/// - 网络状态响应(断网断连、恢复网络立即重连)
|
||||||
/// - 操作前置检查(网络可用性 + 后台状态)
|
/// - 操作前置检查(网络可用性 + 后台状态)
|
||||||
/// - 消息预处理管道(通过 [onMessageTransform] 回调注入解密等)
|
/// - 消息预处理管道(通过 [onMessageTransform] 回调注入解密等)
|
||||||
@@ -38,13 +38,13 @@ typedef MessageTransformer =
|
|||||||
/// ```
|
/// ```
|
||||||
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
||||||
///
|
///
|
||||||
/// ── disconnectInBackground = true(默认,移动端)──
|
/// ── disconnectInBackground = true(后台断连模式)──
|
||||||
/// App 进后台 → onEnterBackground() → 暂停心跳 + 断开连接(省电)
|
/// App 进后台 → onEnterBackground() → 暂停心跳 + 断开连接(省电)
|
||||||
/// App 回前台 → onEnterForeground() → 恢复心跳 → onBeforeReconnect → 重连
|
/// App 回前台 → onEnterForeground() → 恢复心跳 → onBeforeReconnect → 重连
|
||||||
///
|
///
|
||||||
/// ── disconnectInBackground = false(桌面端)──
|
/// ── disconnectInBackground = false(后台保活模式,本项目默认)──
|
||||||
/// App 进后台 → onEnterBackground() → 不操作,完全保活
|
/// App 进后台 → onEnterBackground() → 不操作,心跳不停、连接不断
|
||||||
/// App 回前台 → onEnterForeground() → 不操作(连接始终在线)
|
/// App 回前台 → onEnterForeground() → 检查连接健康,异常则重连
|
||||||
///
|
///
|
||||||
/// 网络丢失 → handleNetworkLost() → 断开连接
|
/// 网络丢失 → handleNetworkLost() → 断开连接
|
||||||
/// 网络恢复 → handleNetworkRestored() → 退避 → onBeforeReconnect → 重连
|
/// 网络恢复 → handleNetworkRestored() → 退避 → onBeforeReconnect → 重连
|
||||||
@@ -86,9 +86,9 @@ class SocketManager {
|
|||||||
|
|
||||||
/// 进后台时是否断开连接
|
/// 进后台时是否断开连接
|
||||||
///
|
///
|
||||||
/// true(默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
|
/// true(SDK 默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
|
||||||
/// false — 后台保持连接(适用于桌面端或需要后台实时推送的场景)。
|
/// false(本项目使用)— 后台保持连接,心跳不停、请求不停,最大程度保活。
|
||||||
/// 设为 false 时,后台仅暂停心跳,不主动断连。
|
/// 回前台时检查连接健康,异常则触发重连。
|
||||||
final bool disconnectInBackground;
|
final bool disconnectInBackground;
|
||||||
|
|
||||||
/// 日志回调
|
/// 日志回调
|
||||||
@@ -147,7 +147,7 @@ class SocketManager {
|
|||||||
_reconnectOnForeground = false;
|
_reconnectOnForeground = false;
|
||||||
_reconnectOnNetworkRestore = false;
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
// 前置检查:移动端模式下在后台不连接(省电)
|
// 前置检查:后台断连模式下在后台不连接(省电)
|
||||||
if (_isInBackground && disconnectInBackground) {
|
if (_isInBackground && disconnectInBackground) {
|
||||||
_reconnectOnForeground = true;
|
_reconnectOnForeground = true;
|
||||||
_log('In background, defer connect to foreground');
|
_log('In background, defer connect to foreground');
|
||||||
@@ -200,18 +200,18 @@ class SocketManager {
|
|||||||
|
|
||||||
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// 后台 → 断连(省电省流量)或保持连接(桌面端)
|
// 后台 → 保活(心跳不停、连接不断)或断连(省电模式)
|
||||||
// 前台 → 自动重连(如果之前有连接)
|
// 前台 → 检查连接健康 / 自动重连
|
||||||
|
|
||||||
/// App 进后台
|
/// App 进后台
|
||||||
///
|
///
|
||||||
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
||||||
///
|
///
|
||||||
/// [disconnectInBackground] 为 true 时(默认,移动端):
|
/// [disconnectInBackground] 为 false 时(后台保活,本项目默认):
|
||||||
/// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
|
|
||||||
///
|
|
||||||
/// [disconnectInBackground] 为 false 时(桌面端):
|
|
||||||
/// 不断连、不暂停心跳,WebSocket 完全保活。
|
/// 不断连、不暂停心跳,WebSocket 完全保活。
|
||||||
|
///
|
||||||
|
/// [disconnectInBackground] 为 true 时(后台断连模式):
|
||||||
|
/// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
|
||||||
void onEnterBackground() {
|
void onEnterBackground() {
|
||||||
_isInBackground = true;
|
_isInBackground = true;
|
||||||
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
||||||
@@ -219,12 +219,12 @@ class SocketManager {
|
|||||||
_foregroundReconnectTimer = null;
|
_foregroundReconnectTimer = null;
|
||||||
|
|
||||||
if (!disconnectInBackground) {
|
if (!disconnectInBackground) {
|
||||||
// 桌面端模式:不断连、不暂停心跳,完全保活
|
// 后台保活模式:不断连、不暂停心跳,不通知 SocketClient
|
||||||
_log('Entering background, keeping connection alive');
|
_log('Entering background, keeping connection alive');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端模式:通知 SocketClient 进后台(暂停心跳)
|
// 后台断连模式:通知 SocketClient 进后台(暂停心跳)
|
||||||
_client.onEnterBackground();
|
_client.onEnterBackground();
|
||||||
|
|
||||||
if (_lastToken == null) return; // 未登录,无需处理
|
if (_lastToken == null) return; // 未登录,无需处理
|
||||||
@@ -240,22 +240,49 @@ class SocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// App 回前台 → 自动重连(如果之前后台断连)
|
/// App 回前台
|
||||||
///
|
///
|
||||||
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.resumed] 时调用。
|
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.resumed] 时调用。
|
||||||
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
|
///
|
||||||
|
/// 后台保活模式(disconnectInBackground=false):
|
||||||
|
/// 检查连接健康,如果后台期间连接意外断开则自动重连。
|
||||||
|
///
|
||||||
|
/// 后台断连模式(disconnectInBackground=true):
|
||||||
|
/// 通知 SocketClient 恢复心跳,然后重新建立连接。
|
||||||
void onEnterForeground() {
|
void onEnterForeground() {
|
||||||
_isInBackground = false;
|
_isInBackground = false;
|
||||||
|
|
||||||
// 只在移动端模式(后台曾断连/暂停心跳)时通知 SocketClient 恢复
|
|
||||||
if (disconnectInBackground) {
|
if (disconnectInBackground) {
|
||||||
|
// 后台断连模式:通知 SocketClient 恢复心跳
|
||||||
_client.onEnterForeground();
|
_client.onEnterForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!disconnectInBackground && _lastToken != null) {
|
||||||
|
// 后台保活模式:检查连接健康
|
||||||
|
// 虽然后台期间心跳不停,但连接仍可能因网络切换、服务端关闭等原因断开。
|
||||||
|
// SocketClient 的自动重连在后台也会工作(_isBackground=false),
|
||||||
|
// 但回前台时兜底检查一次,确保连接可用。
|
||||||
|
if (!_client.isConnected) {
|
||||||
|
_log('Returning to foreground, connection lost, reconnecting...');
|
||||||
|
_scheduleReconnect();
|
||||||
|
} else {
|
||||||
|
_log('Returning to foreground, connection healthy');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_reconnectOnForeground && _lastToken != null) {
|
if (_reconnectOnForeground && _lastToken != null) {
|
||||||
|
// 后台断连模式:之前后台断连过,需要重连
|
||||||
_reconnectOnForeground = false;
|
_reconnectOnForeground = false;
|
||||||
_log('Returning to foreground, reconnecting...');
|
_log('Returning to foreground, reconnecting...');
|
||||||
// 延迟 500ms 等待网络稳定,通过 Timer 跟踪以便进后台时取消
|
_scheduleReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 延迟 500ms 后执行重连
|
||||||
|
///
|
||||||
|
/// 等待网络稳定,通过 Timer 跟踪以便进后台时取消。
|
||||||
|
void _scheduleReconnect() {
|
||||||
_foregroundReconnectTimer?.cancel();
|
_foregroundReconnectTimer?.cancel();
|
||||||
_foregroundReconnectTimer = Timer(
|
_foregroundReconnectTimer = Timer(
|
||||||
const Duration(milliseconds: 500),
|
const Duration(milliseconds: 500),
|
||||||
@@ -284,7 +311,6 @@ class SocketManager {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ── 网络状态变化 ──────────────────────────────────────────────────────────
|
// ── 网络状态变化 ──────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
@@ -328,7 +354,7 @@ class SocketManager {
|
|||||||
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
||||||
_reconnectOnNetworkRestore = false;
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
// 移动端模式:在后台不重连,等前台恢复时再连
|
// 后台断连模式:在后台不重连,等前台恢复时再连
|
||||||
if (_isInBackground && disconnectInBackground) {
|
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');
|
||||||
@@ -415,7 +441,10 @@ class SocketManager {
|
|||||||
|
|
||||||
/// 发送前置检查
|
/// 发送前置检查
|
||||||
///
|
///
|
||||||
/// 两重保险:连接状态 + 后台状态。
|
/// 后台保活模式(disconnectInBackground=false):只检查连接状态,
|
||||||
|
/// 后台也能正常发送。
|
||||||
|
///
|
||||||
|
/// 后台断连模式(disconnectInBackground=true):额外检查后台状态,
|
||||||
/// 后台已断连所以 isConnected 通常就能拦住,
|
/// 后台已断连所以 isConnected 通常就能拦住,
|
||||||
/// 但显式检查 _isInBackground 防止边界情况遗漏。
|
/// 但显式检查 _isInBackground 防止边界情况遗漏。
|
||||||
bool _canSend() {
|
bool _canSend() {
|
||||||
@@ -424,7 +453,7 @@ class SocketManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (_isInBackground && disconnectInBackground) {
|
if (_isInBackground && disconnectInBackground) {
|
||||||
_log('In background, skip send');
|
_log('In background (disconnect mode), skip send');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,27 +1,82 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/favourites.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/sounds.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/tags.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/pending_friend_request_histories.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/message.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/recent_mini_apps.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/retries.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/groups.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/favorite_mini_apps.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/discover_mini_apps.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/chat_categories.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/chat_bots.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/favourite_details.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/user_request_histories.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/workspaces.dart';
|
||||||
import 'package:im_app/data/local/drift/tables/users.dart';
|
import 'package:im_app/data/local/drift/tables/users.dart';
|
||||||
import 'package:im_app/data/local/drift/tables/test_tables.dart';
|
import 'package:im_app/data/local/drift/tables/explore_mini_apps.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/call_logs.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/chats.dart';
|
||||||
|
|
||||||
part 'app_database.g.dart';
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
@DriftDatabase(tables: [Users,TestTables]) //update mapping here
|
@DriftDatabase(
|
||||||
|
tables: [
|
||||||
|
Favourites,
|
||||||
|
Sounds,
|
||||||
|
Tags,
|
||||||
|
PendingFriendRequestHistories,
|
||||||
|
Messages,
|
||||||
|
RecentMiniApps,
|
||||||
|
Retries,
|
||||||
|
Groups,
|
||||||
|
FavoriteMiniApps,
|
||||||
|
DiscoverMiniApps,
|
||||||
|
ChatCategories,
|
||||||
|
ChatBots,
|
||||||
|
FavouriteDetails,
|
||||||
|
UserRequestHistories,
|
||||||
|
Workspaces,
|
||||||
|
Users,
|
||||||
|
ExploreMiniApps,
|
||||||
|
CallLogs,
|
||||||
|
Chats,
|
||||||
|
],
|
||||||
|
) //update mapping here
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
|
||||||
static Map<Type, TableInfo> getTableRegistry(GeneratedDatabase database) {
|
static Map<Type, TableInfo> getTableRegistry(GeneratedDatabase database) {
|
||||||
if (database is! AppDatabase) {
|
if (database is! AppDatabase) {
|
||||||
return {
|
return {};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
User: database.users,
|
DriftFavourite: database.favourites,
|
||||||
TestTable: database.testTables,
|
DriftSound: database.sounds,
|
||||||
|
DriftTag: database.tags,
|
||||||
|
DriftPendingFriendRequestHistory: database.pendingFriendRequestHistories,
|
||||||
|
DriftMessage: database.messages,
|
||||||
|
DriftRecentMiniApp: database.recentMiniApps,
|
||||||
|
DriftRetry: database.retries,
|
||||||
|
DriftGroup: database.groups,
|
||||||
|
DriftFavoriteMiniApp: database.favoriteMiniApps,
|
||||||
|
DriftDiscoverMiniApp: database.discoverMiniApps,
|
||||||
|
DriftChatCategory: database.chatCategories,
|
||||||
|
DriftChatBot: database.chatBots,
|
||||||
|
DriftFavouriteDetail: database.favouriteDetails,
|
||||||
|
DriftUserRequestHistory: database.userRequestHistories,
|
||||||
|
DriftWorkspace: database.workspaces,
|
||||||
|
DriftUser: database.users,
|
||||||
|
DriftExploreMiniApp: database.exploreMiniApps,
|
||||||
|
DriftCallLog: database.callLogs,
|
||||||
|
DriftChat: database.chats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
|
//升级数据库用此版本号
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@@ -30,9 +85,20 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
await m.createAll();
|
await m.createAll();
|
||||||
},
|
},
|
||||||
onUpgrade: (m, from, to) async {
|
onUpgrade: (m, from, to) async {
|
||||||
// 自动检测并添加缺失列
|
// Create any new tables that don't exist yet
|
||||||
for (final table in allTables) {
|
for (final table in allTables) {
|
||||||
//取原来的字段
|
final existingTables = await m.database
|
||||||
|
.customSelect(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='${table.actualTableName}'",
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (existingTables.isEmpty) {
|
||||||
|
await m.createTable(table);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect and add missing columns
|
||||||
final existingColumns = await m.database
|
final existingColumns = await m.database
|
||||||
.customSelect('PRAGMA table_info(${table.actualTableName})')
|
.customSelect('PRAGMA table_info(${table.actualTableName})')
|
||||||
.get();
|
.get();
|
||||||
@@ -42,7 +108,6 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
|
|
||||||
for (final column in table.$columns) {
|
for (final column in table.$columns) {
|
||||||
if (!existingNames.contains(column.name)) {
|
if (!existingNames.contains(column.name)) {
|
||||||
//字段缺失,添加。
|
|
||||||
await m.addColumn(table, column);
|
await m.addColumn(table, column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +115,4 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
apps/im_app/lib/data/local/drift/tables/call_logs.dart
Normal file
24
apps/im_app/lib/data/local/drift/tables/call_logs.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftCallLog')
|
||||||
|
class CallLogs extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
IntColumn get callerId => integer().nullable()();
|
||||||
|
IntColumn get receiverId => integer().nullable()();
|
||||||
|
IntColumn get chatId => integer().nullable()();
|
||||||
|
IntColumn get duration => integer().nullable()();
|
||||||
|
IntColumn get videoCall => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get endedAt => integer().nullable()();
|
||||||
|
IntColumn get status => integer().nullable()();
|
||||||
|
IntColumn get isDeleted => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
IntColumn get isRead => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'call_log';
|
||||||
|
}
|
||||||
33
apps/im_app/lib/data/local/drift/tables/chat_bots.dart
Normal file
33
apps/im_app/lib/data/local/drift/tables/chat_bots.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftChatBot')
|
||||||
|
class ChatBots extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get username => text().nullable()();
|
||||||
|
IntColumn get botUserId => integer().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
TextColumn get token => text().nullable()();
|
||||||
|
IntColumn get flag => integer().nullable()();
|
||||||
|
IntColumn get status => integer().nullable()();
|
||||||
|
TextColumn get webhook => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get commands => text().withDefault(const Constant('[]'))();
|
||||||
|
TextColumn get banner => text().nullable()();
|
||||||
|
IntColumn get channelId => integer().nullable()();
|
||||||
|
IntColumn get channelGroupId => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
TextColumn get internalWebhook => text().nullable()();
|
||||||
|
IntColumn get mode => integer().nullable()();
|
||||||
|
TextColumn get redirectUrl => text().nullable()();
|
||||||
|
IntColumn get isInvitable => integer().nullable()();
|
||||||
|
IntColumn get isAllowForward => integer().nullable()();
|
||||||
|
TextColumn get tips => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'chat_bot';
|
||||||
|
}
|
||||||
20
apps/im_app/lib/data/local/drift/tables/chat_categories.dart
Normal file
20
apps/im_app/lib/data/local/drift/tables/chat_categories.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftChatCategory')
|
||||||
|
class ChatCategories extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get includedChatIds => text().nullable()();
|
||||||
|
TextColumn get excludedChatIds => text().nullable()();
|
||||||
|
IntColumn get seq => integer().nullable()();
|
||||||
|
IntColumn get isHide => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get createdAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get updatedAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get deletedAt => integer().withDefault(const Constant(0))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'chat_category';
|
||||||
|
}
|
||||||
59
apps/im_app/lib/data/local/drift/tables/chats.dart
Normal file
59
apps/im_app/lib/data/local/drift/tables/chats.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftChat')
|
||||||
|
class Chats extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get lastId => integer().nullable()();
|
||||||
|
IntColumn get lastTyp => integer().nullable()();
|
||||||
|
TextColumn get lastMsg => text().nullable()();
|
||||||
|
IntColumn get lastTime => integer().nullable()();
|
||||||
|
IntColumn get lastPos => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get firstPos => integer().withDefault(const Constant(-1))();
|
||||||
|
IntColumn get msgIdx => integer().nullable()();
|
||||||
|
TextColumn get profile => text().nullable()();
|
||||||
|
TextColumn get pin => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
IntColumn get userId => integer().nullable()();
|
||||||
|
IntColumn get chatId => integer().nullable()();
|
||||||
|
IntColumn get friendId => integer().nullable()();
|
||||||
|
IntColumn get sort => integer().nullable()();
|
||||||
|
IntColumn get unreadNum => integer().nullable()();
|
||||||
|
IntColumn get unreadCount => integer().nullable()();
|
||||||
|
IntColumn get hideChatMsgIdx => integer().nullable()();
|
||||||
|
IntColumn get readChatMsgIdx => integer().nullable()();
|
||||||
|
IntColumn get otherReadIdx => integer().nullable()();
|
||||||
|
TextColumn get unreadAtMsgIdx => text().nullable()();
|
||||||
|
IntColumn get deleteTime => integer().nullable()();
|
||||||
|
IntColumn get addIndex => integer().nullable()();
|
||||||
|
IntColumn get flag => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get flagMy => integer().nullable()();
|
||||||
|
IntColumn get autoDeleteInterval => integer().nullable()();
|
||||||
|
IntColumn get mute => integer().nullable()();
|
||||||
|
IntColumn get verified => integer().nullable()();
|
||||||
|
IntColumn get createTime => integer().nullable()();
|
||||||
|
IntColumn get startIdx => integer().nullable()();
|
||||||
|
IntColumn get isReadMsg => integer().nullable()();
|
||||||
|
TextColumn get translateOutgoing => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get translateIncoming => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get incomingIdx => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get outgoingIdx => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get notificationSoundId =>
|
||||||
|
integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get chatKey => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get activeChatKey => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get coverIdx => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get round => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get workspaceId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get localPermission => integer().withDefault(const Constant(0))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'chat';
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftDiscoverMiniApp')
|
||||||
|
class DiscoverMiniApps extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get openuid => text().nullable()();
|
||||||
|
TextColumn get devId => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().nullable()();
|
||||||
|
TextColumn get downloadUrl => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get version => integer().nullable()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get flag => integer().nullable()();
|
||||||
|
IntColumn get reviewStatus => integer().nullable()();
|
||||||
|
IntColumn get favoriteAt => integer().nullable()();
|
||||||
|
IntColumn get isActive => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
RealColumn get score => real().nullable()();
|
||||||
|
TextColumn get channels => text().nullable()();
|
||||||
|
TextColumn get devName => text().nullable()();
|
||||||
|
TextColumn get pictureGaussian => text().nullable()();
|
||||||
|
TextColumn get picture => text().nullable()();
|
||||||
|
IntColumn get commentNum => integer().nullable()();
|
||||||
|
TextColumn get lastLoginAt => text().nullable()();
|
||||||
|
TextColumn get screen => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'discover_mini_app';
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftExploreMiniApp')
|
||||||
|
class ExploreMiniApps extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get openuid => text().nullable()();
|
||||||
|
TextColumn get devId => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().nullable()();
|
||||||
|
TextColumn get downloadUrl => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get version => integer().nullable()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get flag => integer().nullable()();
|
||||||
|
IntColumn get reviewStatus => integer().nullable()();
|
||||||
|
IntColumn get favoriteAt => integer().nullable()();
|
||||||
|
IntColumn get isActive => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
RealColumn get score => real().nullable()();
|
||||||
|
TextColumn get channels => text().nullable()();
|
||||||
|
TextColumn get devName => text().nullable()();
|
||||||
|
TextColumn get pictureGaussian => text().nullable()();
|
||||||
|
TextColumn get picture => text().nullable()();
|
||||||
|
IntColumn get commentNum => integer().nullable()();
|
||||||
|
IntColumn get lastLoginAt => integer().nullable()();
|
||||||
|
TextColumn get screen => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'explore_mini_app';
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftFavoriteMiniApp')
|
||||||
|
class FavoriteMiniApps extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get openuid => text().nullable()();
|
||||||
|
TextColumn get devId => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().nullable()();
|
||||||
|
TextColumn get downloadUrl => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get version => integer().nullable()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get flag => integer().nullable()();
|
||||||
|
IntColumn get reviewStatus => integer().nullable()();
|
||||||
|
IntColumn get favoriteAt => integer().nullable()();
|
||||||
|
IntColumn get isActive => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
RealColumn get score => real().nullable()();
|
||||||
|
TextColumn get channels => text().nullable()();
|
||||||
|
TextColumn get devName => text().nullable()();
|
||||||
|
TextColumn get pictureGaussian => text().nullable()();
|
||||||
|
TextColumn get picture => text().nullable()();
|
||||||
|
IntColumn get commentNum => integer().nullable()();
|
||||||
|
IntColumn get lastLoginAt => integer().nullable()();
|
||||||
|
TextColumn get screen => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'favorite_mini_app';
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftFavouriteDetail')
|
||||||
|
class FavouriteDetails extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get relatedId => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get content => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get messageId => integer().nullable()();
|
||||||
|
IntColumn get sendId => integer().nullable()();
|
||||||
|
IntColumn get chatId => integer().nullable()();
|
||||||
|
IntColumn get sendTime => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'favourite_detail';
|
||||||
|
}
|
||||||
26
apps/im_app/lib/data/local/drift/tables/favourites.dart
Normal file
26
apps/im_app/lib/data/local/drift/tables/favourites.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftFavourite')
|
||||||
|
class Favourites extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
TextColumn get parentId => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get data => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get createdAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get updatedAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get deletedAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get source => integer().nullable()();
|
||||||
|
IntColumn get userId => integer().nullable()();
|
||||||
|
IntColumn get authorId => integer().nullable()();
|
||||||
|
TextColumn get typ => text().withDefault(const Constant('[]'))();
|
||||||
|
TextColumn get tag => text().withDefault(const Constant('[]'))();
|
||||||
|
IntColumn get isPin => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get chatTyp => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get isUploaded => integer().withDefault(const Constant(1))();
|
||||||
|
TextColumn get urls => text().withDefault(const Constant('[]'))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'favourite';
|
||||||
|
}
|
||||||
39
apps/im_app/lib/data/local/drift/tables/groups.dart
Normal file
39
apps/im_app/lib/data/local/drift/tables/groups.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftGroup')
|
||||||
|
class Groups extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
IntColumn get userJoinDate => integer().nullable()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get profile => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get permission => integer().nullable()();
|
||||||
|
IntColumn get admin => integer().nullable()();
|
||||||
|
TextColumn get members => text().nullable()();
|
||||||
|
IntColumn get owner => integer().nullable()();
|
||||||
|
TextColumn get admins => text().nullable()();
|
||||||
|
IntColumn get visible => integer().nullable()();
|
||||||
|
IntColumn get speakInterval => integer().nullable()();
|
||||||
|
IntColumn get groupType => integer().nullable()();
|
||||||
|
IntColumn get roomType => integer().nullable()();
|
||||||
|
IntColumn get maxNumber => integer().nullable()();
|
||||||
|
IntColumn get channelId => integer().nullable()();
|
||||||
|
IntColumn get channelGroupId => integer().nullable()();
|
||||||
|
IntColumn get createTime => integer().nullable()();
|
||||||
|
IntColumn get updateTime => integer().nullable()();
|
||||||
|
IntColumn get addIndex => integer().nullable()();
|
||||||
|
IntColumn get maxMember => integer().nullable()();
|
||||||
|
IntColumn get expireTime => integer().nullable()();
|
||||||
|
IntColumn get workspaceId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get mode => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get redpacketPlay => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get topic => text().nullable()();
|
||||||
|
TextColumn get rp => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'chat_group';
|
||||||
|
}
|
||||||
27
apps/im_app/lib/data/local/drift/tables/message.dart
Normal file
27
apps/im_app/lib/data/local/drift/tables/message.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftMessage')
|
||||||
|
class Messages extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
IntColumn get messageId => integer().nullable()();
|
||||||
|
IntColumn get chatId => integer().nullable()();
|
||||||
|
IntColumn get chatIdx => integer().nullable()();
|
||||||
|
IntColumn get sendId => integer().nullable()();
|
||||||
|
TextColumn get content => text().nullable()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get sendTime => integer().nullable()();
|
||||||
|
IntColumn get expireTime => integer().nullable()();
|
||||||
|
IntColumn get createTime => integer().nullable()();
|
||||||
|
TextColumn get atUsers => text().nullable()();
|
||||||
|
TextColumn get emojis => text().withDefault(const Constant('[]'))();
|
||||||
|
IntColumn get editTime => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get refTyp => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get flag => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get cmid => text().withDefault(const Constant(''))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'message';
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftPendingFriendRequestHistory')
|
||||||
|
class PendingFriendRequestHistories extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
IntColumn get uid => integer()();
|
||||||
|
IntColumn get requestTime => integer()();
|
||||||
|
TextColumn get remarks => text().nullable()();
|
||||||
|
TextColumn get source => text().nullable()();
|
||||||
|
IntColumn get rs => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'pending_friend_request_histories';
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftRecentMiniApp')
|
||||||
|
class RecentMiniApps extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get openuid => text().nullable()();
|
||||||
|
TextColumn get devId => text().nullable()();
|
||||||
|
TextColumn get icon => text().nullable()();
|
||||||
|
TextColumn get iconGaussian => text().nullable()();
|
||||||
|
TextColumn get downloadUrl => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get version => integer().nullable()();
|
||||||
|
IntColumn get typ => integer().nullable()();
|
||||||
|
IntColumn get flag => integer().nullable()();
|
||||||
|
IntColumn get reviewStatus => integer().nullable()();
|
||||||
|
IntColumn get favoriteAt => integer().nullable()();
|
||||||
|
IntColumn get isActive => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
RealColumn get score => real().nullable()();
|
||||||
|
TextColumn get channels => text().nullable()();
|
||||||
|
TextColumn get devName => text().nullable()();
|
||||||
|
TextColumn get pictureGaussian => text().nullable()();
|
||||||
|
TextColumn get picture => text().nullable()();
|
||||||
|
IntColumn get commentNum => integer().nullable()();
|
||||||
|
IntColumn get lastLoginAt => integer().nullable()();
|
||||||
|
TextColumn get screen => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'recent_mini_app';
|
||||||
|
}
|
||||||
20
apps/im_app/lib/data/local/drift/tables/retries.dart
Normal file
20
apps/im_app/lib/data/local/drift/tables/retries.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftRetry')
|
||||||
|
class Retries extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get uid => integer().nullable()();
|
||||||
|
TextColumn get apiType => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get endPoint => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get requestData => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get synced => integer().nullable()();
|
||||||
|
TextColumn get callbackFun => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get expired => integer().nullable()();
|
||||||
|
IntColumn get replace => integer().nullable()();
|
||||||
|
IntColumn get expireTime => integer().nullable()();
|
||||||
|
IntColumn get createTime => integer().nullable()();
|
||||||
|
IntColumn get addIndex => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'retry';
|
||||||
|
}
|
||||||
20
apps/im_app/lib/data/local/drift/tables/sounds.dart
Normal file
20
apps/im_app/lib/data/local/drift/tables/sounds.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftSound')
|
||||||
|
class Sounds extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
TextColumn get filePath => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get typ => integer()();
|
||||||
|
TextColumn get name => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get createdAt => integer()();
|
||||||
|
IntColumn get updatedAt => integer()();
|
||||||
|
IntColumn get deletedAt => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get channelGroupId => integer()();
|
||||||
|
IntColumn get isDefault => integer()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'sound';
|
||||||
|
}
|
||||||
15
apps/im_app/lib/data/local/drift/tables/tags.dart
Normal file
15
apps/im_app/lib/data/local/drift/tables/tags.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftTag')
|
||||||
|
class Tags extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get uid => integer().nullable()();
|
||||||
|
TextColumn get name => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get type => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get addIndex => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'tags';
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
|
|
||||||
@DataClassName('TestTable')
|
|
||||||
class TestTables extends Table {
|
|
||||||
IntColumn get id => integer().autoIncrement()();
|
|
||||||
IntColumn get uid => integer().nullable()();
|
|
||||||
TextColumn get uuid => text().nullable()();
|
|
||||||
IntColumn get lastOnline => integer().nullable()();
|
|
||||||
TextColumn get profilePic => text().nullable()();
|
|
||||||
TextColumn get profilePicGaussian => text().withDefault(const Constant(''))();
|
|
||||||
TextColumn get nickname => text().nullable()();
|
|
||||||
TextColumn get depositName => text().nullable()();
|
|
||||||
IntColumn get hasSetDepositName => integer().withDefault(const Constant(0))();
|
|
||||||
TextColumn get contact => text().nullable()();
|
|
||||||
TextColumn get countryCode => text().nullable()();
|
|
||||||
TextColumn get username => text().nullable()();
|
|
||||||
IntColumn get role => integer().nullable()();
|
|
||||||
IntColumn get relationship => integer().nullable()();
|
|
||||||
IntColumn get friendStatus => integer().nullable()();
|
|
||||||
TextColumn get bio => text().nullable()();
|
|
||||||
TextColumn get userAlias => text().nullable()();
|
|
||||||
IntColumn get requestAt => integer().nullable()();
|
|
||||||
IntColumn get deletedAt => integer().nullable()();
|
|
||||||
TextColumn get email => text().nullable()();
|
|
||||||
TextColumn get recoveryEmail => text().nullable()();
|
|
||||||
TextColumn get remark => text().nullable()();
|
|
||||||
TextColumn get source => text().nullable()();
|
|
||||||
IntColumn get addIndex => integer().nullable()();
|
|
||||||
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
|
||||||
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
|
||||||
IntColumn get notificationSoundId => integer().withDefault(const Constant(0))();
|
|
||||||
IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))();
|
|
||||||
IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))();
|
|
||||||
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
|
|
||||||
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
|
|
||||||
TextColumn get publicKey => text().nullable()();
|
|
||||||
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
|
||||||
TextColumn get hint => text().nullable()();
|
|
||||||
@override
|
|
||||||
String get tableName => 'test_tables';
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftUserRequestHistory')
|
||||||
|
class UserRequestHistories extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
IntColumn get status => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'user_request_history';
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
@DataClassName('User')
|
@DataClassName('DriftUser')
|
||||||
class Users extends Table {
|
class Users extends Table {
|
||||||
IntColumn get id => integer().autoIncrement()();
|
IntColumn get id => integer().autoIncrement()();
|
||||||
IntColumn get uid => integer().nullable()();
|
IntColumn get uid => integer().unique()();
|
||||||
TextColumn get uuid => text().nullable()();
|
TextColumn get uuid => text().nullable()();
|
||||||
IntColumn get lastOnline => integer().nullable()();
|
IntColumn get lastOnline => integer().nullable()();
|
||||||
TextColumn get profilePic => text().nullable()();
|
TextColumn get profilePic => text().nullable()();
|
||||||
@@ -28,14 +28,18 @@ class Users extends Table {
|
|||||||
IntColumn get addIndex => integer().nullable()();
|
IntColumn get addIndex => integer().nullable()();
|
||||||
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
||||||
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
||||||
IntColumn get notificationSoundId => integer().withDefault(const Constant(0))();
|
IntColumn get notificationSoundId =>
|
||||||
IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))();
|
integer().withDefault(const Constant(0))();
|
||||||
IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))();
|
IntColumn get sendMessageSoundId =>
|
||||||
|
integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get groupNotificationSoundId =>
|
||||||
|
integer().withDefault(const Constant(0))();
|
||||||
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
|
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
|
||||||
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
|
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
|
||||||
TextColumn get publicKey => text().nullable()();
|
TextColumn get publicKey => text().nullable()();
|
||||||
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
||||||
TextColumn get hint => text().nullable()();
|
TextColumn get hint => text().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tableName => 'user';
|
String get tableName => 'user';
|
||||||
}
|
}
|
||||||
24
apps/im_app/lib/data/local/drift/tables/workspaces.dart
Normal file
24
apps/im_app/lib/data/local/drift/tables/workspaces.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('DriftWorkspace')
|
||||||
|
class Workspaces extends Table {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
IntColumn get ownerId => integer().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
TextColumn get logo => text().nullable()();
|
||||||
|
IntColumn get grade => integer().nullable()();
|
||||||
|
IntColumn get cap => integer().nullable()();
|
||||||
|
TextColumn get currency => text().nullable()();
|
||||||
|
IntColumn get status => integer().nullable()();
|
||||||
|
IntColumn get createdAt => integer().nullable()();
|
||||||
|
IntColumn get updatedAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
IntColumn get channelGroupId => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tableName => 'workspace';
|
||||||
|
}
|
||||||
122
apps/im_app/lib/data/models/call_log_dto.dart
Normal file
122
apps/im_app/lib/data/models/call_log_dto.dart
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/call_log.dart';
|
||||||
|
|
||||||
|
/// 通话记录 DTO(Data Transfer Object)
|
||||||
|
///
|
||||||
|
/// local / remote 共用的数据传输对象。
|
||||||
|
/// 提供与 Domain Entity [CallLog] 之间的双向转换。
|
||||||
|
class CallLogDto {
|
||||||
|
final String id;
|
||||||
|
final int? callerId;
|
||||||
|
final int? receiverId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? duration;
|
||||||
|
final int? videoCall;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? endedAt;
|
||||||
|
final int? status;
|
||||||
|
final int? isDeleted;
|
||||||
|
final int? deletedAt;
|
||||||
|
final int? isRead;
|
||||||
|
|
||||||
|
const CallLogDto({
|
||||||
|
required this.id,
|
||||||
|
this.callerId,
|
||||||
|
this.receiverId,
|
||||||
|
this.chatId,
|
||||||
|
this.duration,
|
||||||
|
this.videoCall,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.endedAt,
|
||||||
|
this.status,
|
||||||
|
this.isDeleted,
|
||||||
|
this.deletedAt,
|
||||||
|
this.isRead,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CallLogDto.fromJson(Map<String, dynamic> json) => CallLogDto(
|
||||||
|
id: json['id'] as String,
|
||||||
|
callerId: json['caller_id'],
|
||||||
|
receiverId: json['receiver_id'],
|
||||||
|
chatId: json['chat_id'],
|
||||||
|
duration: json['duration'],
|
||||||
|
videoCall: json['video_call'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
endedAt: json['ended_at'],
|
||||||
|
status: json['status'],
|
||||||
|
isDeleted: json['is_deleted'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
isRead: json['is_read'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'caller_id': callerId,
|
||||||
|
'receiver_id': receiverId,
|
||||||
|
'chat_id': chatId,
|
||||||
|
'duration': duration,
|
||||||
|
'video_call': videoCall,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'ended_at': endedAt,
|
||||||
|
'status': status,
|
||||||
|
'is_deleted': isDeleted,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'is_read': isRead,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// DTO → Domain Entity
|
||||||
|
CallLog toEntity() => CallLog(
|
||||||
|
id: id,
|
||||||
|
callerId: callerId,
|
||||||
|
receiverId: receiverId,
|
||||||
|
chatId: chatId,
|
||||||
|
duration: duration,
|
||||||
|
videoCall: videoCall,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
endedAt: endedAt,
|
||||||
|
status: status,
|
||||||
|
isDeleted: isDeleted,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
isRead: isRead,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Domain Entity → DTO
|
||||||
|
factory CallLogDto.fromEntity(CallLog callLog) => CallLogDto(
|
||||||
|
id: callLog.id,
|
||||||
|
callerId: callLog.callerId,
|
||||||
|
receiverId: callLog.receiverId,
|
||||||
|
chatId: callLog.chatId,
|
||||||
|
duration: callLog.duration,
|
||||||
|
videoCall: callLog.videoCall,
|
||||||
|
createdAt: callLog.createdAt,
|
||||||
|
updatedAt: callLog.updatedAt,
|
||||||
|
endedAt: callLog.endedAt,
|
||||||
|
status: callLog.status,
|
||||||
|
isDeleted: callLog.isDeleted,
|
||||||
|
deletedAt: callLog.deletedAt,
|
||||||
|
isRead: callLog.isRead,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// DTO → Drift Companion (for DB insert/update)
|
||||||
|
CallLogsCompanion toCompanion() => CallLogsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
callerId: Value(callerId),
|
||||||
|
receiverId: Value(receiverId),
|
||||||
|
chatId: Value(chatId),
|
||||||
|
duration: Value(duration),
|
||||||
|
videoCall: Value(videoCall),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
endedAt: Value(endedAt),
|
||||||
|
status: Value(status),
|
||||||
|
isDeleted: Value(isDeleted),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
isRead: Value(isRead),
|
||||||
|
);
|
||||||
|
}
|
||||||
179
apps/im_app/lib/data/models/chat_bot_dto.dart
Normal file
179
apps/im_app/lib/data/models/chat_bot_dto.dart
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/chat_bot.dart';
|
||||||
|
|
||||||
|
/// 聊天机器人 DTO
|
||||||
|
class ChatBotDto {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final String? username;
|
||||||
|
final int? botUserId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? description;
|
||||||
|
final String? token;
|
||||||
|
final int? flag;
|
||||||
|
final int? status;
|
||||||
|
final String? webhook;
|
||||||
|
final String? commands;
|
||||||
|
final String? banner;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final int? deletedAt;
|
||||||
|
final String? internalWebhook;
|
||||||
|
final int? mode;
|
||||||
|
final String? redirectUrl;
|
||||||
|
final int? isInvitable;
|
||||||
|
final int? isAllowForward;
|
||||||
|
final String? tips;
|
||||||
|
|
||||||
|
const ChatBotDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.username,
|
||||||
|
this.botUserId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.description,
|
||||||
|
this.token,
|
||||||
|
this.flag,
|
||||||
|
this.status,
|
||||||
|
this.webhook,
|
||||||
|
this.commands,
|
||||||
|
this.banner,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.deletedAt,
|
||||||
|
this.internalWebhook,
|
||||||
|
this.mode,
|
||||||
|
this.redirectUrl,
|
||||||
|
this.isInvitable,
|
||||||
|
this.isAllowForward,
|
||||||
|
this.tips,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChatBotDto.fromJson(Map<String, dynamic> json) => ChatBotDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
name: json['name'],
|
||||||
|
username: json['username'],
|
||||||
|
botUserId: json['bot_user_id'],
|
||||||
|
icon: json['icon'],
|
||||||
|
iconGaussian: json['icon_gaussian'],
|
||||||
|
description: json['description'],
|
||||||
|
token: json['token'],
|
||||||
|
flag: json['flag'],
|
||||||
|
status: json['status'],
|
||||||
|
webhook: json['webhook'],
|
||||||
|
commands: json['commands'],
|
||||||
|
banner: json['banner'],
|
||||||
|
channelId: json['channel_id'],
|
||||||
|
channelGroupId: json['channel_group_id'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
internalWebhook: json['internal_webhook'],
|
||||||
|
mode: json['mode'],
|
||||||
|
redirectUrl: json['redirect_url'],
|
||||||
|
isInvitable: json['is_invitable'],
|
||||||
|
isAllowForward: json['is_allow_forward'],
|
||||||
|
tips: json['tips'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'username': username,
|
||||||
|
'bot_user_id': botUserId,
|
||||||
|
'icon': icon,
|
||||||
|
'icon_gaussian': iconGaussian,
|
||||||
|
'description': description,
|
||||||
|
'token': token,
|
||||||
|
'flag': flag,
|
||||||
|
'status': status,
|
||||||
|
'webhook': webhook,
|
||||||
|
'commands': commands,
|
||||||
|
'banner': banner,
|
||||||
|
'channel_id': channelId,
|
||||||
|
'channel_group_id': channelGroupId,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'internal_webhook': internalWebhook,
|
||||||
|
'mode': mode,
|
||||||
|
'redirect_url': redirectUrl,
|
||||||
|
'is_invitable': isInvitable,
|
||||||
|
'is_allow_forward': isAllowForward,
|
||||||
|
'tips': tips,
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatBot toEntity() => ChatBot(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
username: username,
|
||||||
|
botUserId: botUserId,
|
||||||
|
icon: icon,
|
||||||
|
iconGaussian: iconGaussian,
|
||||||
|
description: description,
|
||||||
|
token: token,
|
||||||
|
flag: flag,
|
||||||
|
status: status,
|
||||||
|
webhook: webhook,
|
||||||
|
commands: commands,
|
||||||
|
banner: banner,
|
||||||
|
channelId: channelId,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
internalWebhook: internalWebhook,
|
||||||
|
mode: mode,
|
||||||
|
redirectUrl: redirectUrl,
|
||||||
|
isInvitable: isInvitable,
|
||||||
|
isAllowForward: isAllowForward,
|
||||||
|
tips: tips,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ChatBotDto.fromEntity(ChatBot chatBot) => ChatBotDto(
|
||||||
|
id: chatBot.id,
|
||||||
|
name: chatBot.name,
|
||||||
|
username: chatBot.username,
|
||||||
|
botUserId: chatBot.botUserId,
|
||||||
|
icon: chatBot.icon,
|
||||||
|
iconGaussian: chatBot.iconGaussian,
|
||||||
|
description: chatBot.description,
|
||||||
|
token: chatBot.token,
|
||||||
|
flag: chatBot.flag,
|
||||||
|
status: chatBot.status,
|
||||||
|
webhook: chatBot.webhook,
|
||||||
|
commands: chatBot.commands,
|
||||||
|
banner: chatBot.banner,
|
||||||
|
channelId: chatBot.channelId,
|
||||||
|
channelGroupId: chatBot.channelGroupId,
|
||||||
|
deletedAt: chatBot.deletedAt,
|
||||||
|
internalWebhook: chatBot.internalWebhook,
|
||||||
|
mode: chatBot.mode,
|
||||||
|
redirectUrl: chatBot.redirectUrl,
|
||||||
|
isInvitable: chatBot.isInvitable,
|
||||||
|
isAllowForward: chatBot.isAllowForward,
|
||||||
|
tips: chatBot.tips,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatBotsCompanion toCompanion() => ChatBotsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
username: Value(username),
|
||||||
|
botUserId: Value(botUserId),
|
||||||
|
icon: Value(icon),
|
||||||
|
iconGaussian: Value(iconGaussian),
|
||||||
|
description: Value(description),
|
||||||
|
token: Value(token),
|
||||||
|
flag: Value(flag),
|
||||||
|
status: Value(status),
|
||||||
|
webhook: Value(webhook ?? ''),
|
||||||
|
commands: Value(commands ?? '[]'),
|
||||||
|
banner: Value(banner),
|
||||||
|
channelId: Value(channelId),
|
||||||
|
channelGroupId: Value(channelGroupId),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
internalWebhook: Value(internalWebhook),
|
||||||
|
mode: Value(mode),
|
||||||
|
redirectUrl: Value(redirectUrl),
|
||||||
|
isInvitable: Value(isInvitable),
|
||||||
|
isAllowForward: Value(isAllowForward),
|
||||||
|
tips: Value(tips),
|
||||||
|
);
|
||||||
|
}
|
||||||
90
apps/im_app/lib/data/models/chat_category_dto.dart
Normal file
90
apps/im_app/lib/data/models/chat_category_dto.dart
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/chat_category.dart';
|
||||||
|
|
||||||
|
/// 聊天分类 DTO
|
||||||
|
class ChatCategoryDto {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final String? includedChatIds;
|
||||||
|
final String? excludedChatIds;
|
||||||
|
final int? seq;
|
||||||
|
final int isHide;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
|
||||||
|
const ChatCategoryDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.includedChatIds,
|
||||||
|
this.excludedChatIds,
|
||||||
|
this.seq,
|
||||||
|
this.isHide = 0,
|
||||||
|
this.createdAt = 0,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChatCategoryDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ChatCategoryDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
name: json['name'],
|
||||||
|
includedChatIds: json['included_chat_ids'],
|
||||||
|
excludedChatIds: json['excluded_chat_ids'],
|
||||||
|
seq: json['seq'],
|
||||||
|
isHide: json['is_hide'] ?? 0,
|
||||||
|
createdAt: json['created_at'] ?? 0,
|
||||||
|
updatedAt: json['updated_at'] ?? 0,
|
||||||
|
deletedAt: json['deleted_at'] ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'included_chat_ids': includedChatIds,
|
||||||
|
'excluded_chat_ids': excludedChatIds,
|
||||||
|
'seq': seq,
|
||||||
|
'is_hide': isHide,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatCategory toEntity() => ChatCategory(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
includedChatIds: includedChatIds,
|
||||||
|
excludedChatIds: excludedChatIds,
|
||||||
|
seq: seq,
|
||||||
|
isHide: isHide,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ChatCategoryDto.fromEntity(ChatCategory chatCategory) =>
|
||||||
|
ChatCategoryDto(
|
||||||
|
id: chatCategory.id,
|
||||||
|
name: chatCategory.name,
|
||||||
|
includedChatIds: chatCategory.includedChatIds,
|
||||||
|
excludedChatIds: chatCategory.excludedChatIds,
|
||||||
|
seq: chatCategory.seq,
|
||||||
|
isHide: chatCategory.isHide,
|
||||||
|
createdAt: chatCategory.createdAt,
|
||||||
|
updatedAt: chatCategory.updatedAt,
|
||||||
|
deletedAt: chatCategory.deletedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatCategoriesCompanion toCompanion() => ChatCategoriesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
includedChatIds: Value(includedChatIds),
|
||||||
|
excludedChatIds: Value(excludedChatIds),
|
||||||
|
seq: Value(seq),
|
||||||
|
isHide: Value(isHide),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
200
apps/im_app/lib/data/models/chat_dto.dart
Normal file
200
apps/im_app/lib/data/models/chat_dto.dart
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/// 聊天 Domain 实体
|
||||||
|
class Chat {
|
||||||
|
final int id;
|
||||||
|
final int? typ;
|
||||||
|
final int? lastId;
|
||||||
|
final int? lastTyp;
|
||||||
|
final String? lastMsg;
|
||||||
|
final int? lastTime;
|
||||||
|
final int lastPos;
|
||||||
|
final int firstPos;
|
||||||
|
final int? msgIdx;
|
||||||
|
final String? profile;
|
||||||
|
final String? pin;
|
||||||
|
final String? icon;
|
||||||
|
final String iconGaussian;
|
||||||
|
final String? name;
|
||||||
|
final int? userId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? friendId;
|
||||||
|
final int? sort;
|
||||||
|
final int? unreadNum;
|
||||||
|
final int? unreadCount;
|
||||||
|
final int? hideChatMsgIdx;
|
||||||
|
final int? readChatMsgIdx;
|
||||||
|
final int? otherReadIdx;
|
||||||
|
final String? unreadAtMsgIdx;
|
||||||
|
final int? deleteTime;
|
||||||
|
final int? addIndex;
|
||||||
|
final int flag;
|
||||||
|
final int? flagMy;
|
||||||
|
final int? autoDeleteInterval;
|
||||||
|
final int? mute;
|
||||||
|
final int? verified;
|
||||||
|
final int? createTime;
|
||||||
|
final int? startIdx;
|
||||||
|
final int? isReadMsg;
|
||||||
|
final String translateOutgoing;
|
||||||
|
final String translateIncoming;
|
||||||
|
final int incomingIdx;
|
||||||
|
final int outgoingIdx;
|
||||||
|
final int incomingSoundId;
|
||||||
|
final int outgoingSoundId;
|
||||||
|
final int notificationSoundId;
|
||||||
|
final String chatKey;
|
||||||
|
final String activeChatKey;
|
||||||
|
final int coverIdx;
|
||||||
|
final int round;
|
||||||
|
final int workspaceId;
|
||||||
|
final int localPermission;
|
||||||
|
|
||||||
|
const Chat({
|
||||||
|
required this.id,
|
||||||
|
this.typ,
|
||||||
|
this.lastId,
|
||||||
|
this.lastTyp,
|
||||||
|
this.lastMsg,
|
||||||
|
this.lastTime,
|
||||||
|
this.lastPos = 0,
|
||||||
|
this.firstPos = -1,
|
||||||
|
this.msgIdx,
|
||||||
|
this.profile,
|
||||||
|
this.pin,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian = '',
|
||||||
|
this.name,
|
||||||
|
this.userId,
|
||||||
|
this.chatId,
|
||||||
|
this.friendId,
|
||||||
|
this.sort,
|
||||||
|
this.unreadNum,
|
||||||
|
this.unreadCount,
|
||||||
|
this.hideChatMsgIdx,
|
||||||
|
this.readChatMsgIdx,
|
||||||
|
this.otherReadIdx,
|
||||||
|
this.unreadAtMsgIdx,
|
||||||
|
this.deleteTime,
|
||||||
|
this.addIndex,
|
||||||
|
this.flag = 0,
|
||||||
|
this.flagMy,
|
||||||
|
this.autoDeleteInterval,
|
||||||
|
this.mute,
|
||||||
|
this.verified,
|
||||||
|
this.createTime,
|
||||||
|
this.startIdx,
|
||||||
|
this.isReadMsg,
|
||||||
|
this.translateOutgoing = '',
|
||||||
|
this.translateIncoming = '',
|
||||||
|
this.incomingIdx = 0,
|
||||||
|
this.outgoingIdx = 0,
|
||||||
|
this.incomingSoundId = 0,
|
||||||
|
this.outgoingSoundId = 0,
|
||||||
|
this.notificationSoundId = 0,
|
||||||
|
this.chatKey = '',
|
||||||
|
this.activeChatKey = '',
|
||||||
|
this.coverIdx = 0,
|
||||||
|
this.round = 0,
|
||||||
|
this.workspaceId = 0,
|
||||||
|
this.localPermission = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
Chat copyWith({
|
||||||
|
int? id,
|
||||||
|
int? typ,
|
||||||
|
int? lastId,
|
||||||
|
int? lastTyp,
|
||||||
|
String? lastMsg,
|
||||||
|
int? lastTime,
|
||||||
|
int? lastPos,
|
||||||
|
int? firstPos,
|
||||||
|
int? msgIdx,
|
||||||
|
String? profile,
|
||||||
|
String? pin,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? name,
|
||||||
|
int? userId,
|
||||||
|
int? chatId,
|
||||||
|
int? friendId,
|
||||||
|
int? sort,
|
||||||
|
int? unreadNum,
|
||||||
|
int? unreadCount,
|
||||||
|
int? hideChatMsgIdx,
|
||||||
|
int? readChatMsgIdx,
|
||||||
|
int? otherReadIdx,
|
||||||
|
String? unreadAtMsgIdx,
|
||||||
|
int? deleteTime,
|
||||||
|
int? addIndex,
|
||||||
|
int? flag,
|
||||||
|
int? flagMy,
|
||||||
|
int? autoDeleteInterval,
|
||||||
|
int? mute,
|
||||||
|
int? verified,
|
||||||
|
int? createTime,
|
||||||
|
int? startIdx,
|
||||||
|
int? isReadMsg,
|
||||||
|
String? translateOutgoing,
|
||||||
|
String? translateIncoming,
|
||||||
|
int? incomingIdx,
|
||||||
|
int? outgoingIdx,
|
||||||
|
int? incomingSoundId,
|
||||||
|
int? outgoingSoundId,
|
||||||
|
int? notificationSoundId,
|
||||||
|
String? chatKey,
|
||||||
|
String? activeChatKey,
|
||||||
|
int? coverIdx,
|
||||||
|
int? round,
|
||||||
|
int? workspaceId,
|
||||||
|
int? localPermission,
|
||||||
|
}) {
|
||||||
|
return Chat(
|
||||||
|
id: id ?? this.id,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
lastId: lastId ?? this.lastId,
|
||||||
|
lastTyp: lastTyp ?? this.lastTyp,
|
||||||
|
lastMsg: lastMsg ?? this.lastMsg,
|
||||||
|
lastTime: lastTime ?? this.lastTime,
|
||||||
|
lastPos: lastPos ?? this.lastPos,
|
||||||
|
firstPos: firstPos ?? this.firstPos,
|
||||||
|
msgIdx: msgIdx ?? this.msgIdx,
|
||||||
|
profile: profile ?? this.profile,
|
||||||
|
pin: pin ?? this.pin,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
name: name ?? this.name,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
friendId: friendId ?? this.friendId,
|
||||||
|
sort: sort ?? this.sort,
|
||||||
|
unreadNum: unreadNum ?? this.unreadNum,
|
||||||
|
unreadCount: unreadCount ?? this.unreadCount,
|
||||||
|
hideChatMsgIdx: hideChatMsgIdx ?? this.hideChatMsgIdx,
|
||||||
|
readChatMsgIdx: readChatMsgIdx ?? this.readChatMsgIdx,
|
||||||
|
otherReadIdx: otherReadIdx ?? this.otherReadIdx,
|
||||||
|
unreadAtMsgIdx: unreadAtMsgIdx ?? this.unreadAtMsgIdx,
|
||||||
|
deleteTime: deleteTime ?? this.deleteTime,
|
||||||
|
addIndex: addIndex ?? this.addIndex,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
flagMy: flagMy ?? this.flagMy,
|
||||||
|
autoDeleteInterval: autoDeleteInterval ?? this.autoDeleteInterval,
|
||||||
|
mute: mute ?? this.mute,
|
||||||
|
verified: verified ?? this.verified,
|
||||||
|
createTime: createTime ?? this.createTime,
|
||||||
|
startIdx: startIdx ?? this.startIdx,
|
||||||
|
isReadMsg: isReadMsg ?? this.isReadMsg,
|
||||||
|
translateOutgoing: translateOutgoing ?? this.translateOutgoing,
|
||||||
|
translateIncoming: translateIncoming ?? this.translateIncoming,
|
||||||
|
incomingIdx: incomingIdx ?? this.incomingIdx,
|
||||||
|
outgoingIdx: outgoingIdx ?? this.outgoingIdx,
|
||||||
|
incomingSoundId: incomingSoundId ?? this.incomingSoundId,
|
||||||
|
outgoingSoundId: outgoingSoundId ?? this.outgoingSoundId,
|
||||||
|
notificationSoundId: notificationSoundId ?? this.notificationSoundId,
|
||||||
|
chatKey: chatKey ?? this.chatKey,
|
||||||
|
activeChatKey: activeChatKey ?? this.activeChatKey,
|
||||||
|
coverIdx: coverIdx ?? this.coverIdx,
|
||||||
|
round: round ?? this.round,
|
||||||
|
workspaceId: workspaceId ?? this.workspaceId,
|
||||||
|
localPermission: localPermission ?? this.localPermission,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/im_app/lib/data/models/discover_mini_app_dto.dart
Normal file
112
apps/im_app/lib/data/models/discover_mini_app_dto.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// 发现小程序 Domain 实体
|
||||||
|
class DiscoverMiniApp {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final String? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const DiscoverMiniApp({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
DiscoverMiniApp copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? openuid,
|
||||||
|
String? devId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? downloadUrl,
|
||||||
|
String? description,
|
||||||
|
int? version,
|
||||||
|
int? typ,
|
||||||
|
int? flag,
|
||||||
|
int? reviewStatus,
|
||||||
|
int? favoriteAt,
|
||||||
|
int? isActive,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
double? score,
|
||||||
|
String? channels,
|
||||||
|
String? devName,
|
||||||
|
String? pictureGaussian,
|
||||||
|
String? picture,
|
||||||
|
int? commentNum,
|
||||||
|
String? lastLoginAt,
|
||||||
|
String? screen,
|
||||||
|
}) {
|
||||||
|
return DiscoverMiniApp(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
openuid: openuid ?? this.openuid,
|
||||||
|
devId: devId ?? this.devId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
reviewStatus: reviewStatus ?? this.reviewStatus,
|
||||||
|
favoriteAt: favoriteAt ?? this.favoriteAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
score: score ?? this.score,
|
||||||
|
channels: channels ?? this.channels,
|
||||||
|
devName: devName ?? this.devName,
|
||||||
|
pictureGaussian: pictureGaussian ?? this.pictureGaussian,
|
||||||
|
picture: picture ?? this.picture,
|
||||||
|
commentNum: commentNum ?? this.commentNum,
|
||||||
|
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||||
|
screen: screen ?? this.screen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
apps/im_app/lib/data/models/explore_mini_app_dto.dart
Normal file
201
apps/im_app/lib/data/models/explore_mini_app_dto.dart
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/explore_mini_app.dart';
|
||||||
|
|
||||||
|
/// 探索小程序 DTO
|
||||||
|
class ExploreMiniAppDto {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const ExploreMiniAppDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ExploreMiniAppDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ExploreMiniAppDto(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'],
|
||||||
|
openuid: json['openuid'],
|
||||||
|
devId: json['dev_id'],
|
||||||
|
icon: json['icon'],
|
||||||
|
iconGaussian: json['icon_gaussian'],
|
||||||
|
downloadUrl: json['download_url'],
|
||||||
|
description: json['description'],
|
||||||
|
version: json['version'],
|
||||||
|
typ: json['typ'],
|
||||||
|
flag: json['flag'],
|
||||||
|
reviewStatus: json['review_status'],
|
||||||
|
favoriteAt: json['favorite_at'],
|
||||||
|
isActive: json['is_active'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
score: json['score']?.toDouble(),
|
||||||
|
channels: json['channels'],
|
||||||
|
devName: json['dev_name'],
|
||||||
|
pictureGaussian: json['picture_gaussian'],
|
||||||
|
picture: json['picture'],
|
||||||
|
commentNum: json['comment_num'],
|
||||||
|
lastLoginAt: json['last_login_at'],
|
||||||
|
screen: json['screen'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'openuid': openuid,
|
||||||
|
'dev_id': devId,
|
||||||
|
'icon': icon,
|
||||||
|
'icon_gaussian': iconGaussian,
|
||||||
|
'download_url': downloadUrl,
|
||||||
|
'description': description,
|
||||||
|
'version': version,
|
||||||
|
'typ': typ,
|
||||||
|
'flag': flag,
|
||||||
|
'review_status': reviewStatus,
|
||||||
|
'favorite_at': favoriteAt,
|
||||||
|
'is_active': isActive,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'score': score,
|
||||||
|
'channels': channels,
|
||||||
|
'dev_name': devName,
|
||||||
|
'picture_gaussian': pictureGaussian,
|
||||||
|
'picture': picture,
|
||||||
|
'comment_num': commentNum,
|
||||||
|
'last_login_at': lastLoginAt,
|
||||||
|
'screen': screen,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExploreMiniApp toEntity() => ExploreMiniApp(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
openuid: openuid,
|
||||||
|
devId: devId,
|
||||||
|
icon: icon,
|
||||||
|
iconGaussian: iconGaussian,
|
||||||
|
downloadUrl: downloadUrl,
|
||||||
|
description: description,
|
||||||
|
version: version,
|
||||||
|
typ: typ,
|
||||||
|
flag: flag,
|
||||||
|
reviewStatus: reviewStatus,
|
||||||
|
favoriteAt: favoriteAt,
|
||||||
|
isActive: isActive,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
score: score,
|
||||||
|
channels: channels,
|
||||||
|
devName: devName,
|
||||||
|
pictureGaussian: pictureGaussian,
|
||||||
|
picture: picture,
|
||||||
|
commentNum: commentNum,
|
||||||
|
lastLoginAt: lastLoginAt,
|
||||||
|
screen: screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ExploreMiniAppDto.fromEntity(ExploreMiniApp app) => ExploreMiniAppDto(
|
||||||
|
id: app.id,
|
||||||
|
name: app.name,
|
||||||
|
openuid: app.openuid,
|
||||||
|
devId: app.devId,
|
||||||
|
icon: app.icon,
|
||||||
|
iconGaussian: app.iconGaussian,
|
||||||
|
downloadUrl: app.downloadUrl,
|
||||||
|
description: app.description,
|
||||||
|
version: app.version,
|
||||||
|
typ: app.typ,
|
||||||
|
flag: app.flag,
|
||||||
|
reviewStatus: app.reviewStatus,
|
||||||
|
favoriteAt: app.favoriteAt,
|
||||||
|
isActive: app.isActive,
|
||||||
|
createdAt: app.createdAt,
|
||||||
|
updatedAt: app.updatedAt,
|
||||||
|
deletedAt: app.deletedAt,
|
||||||
|
score: app.score,
|
||||||
|
channels: app.channels,
|
||||||
|
devName: app.devName,
|
||||||
|
pictureGaussian: app.pictureGaussian,
|
||||||
|
picture: app.picture,
|
||||||
|
commentNum: app.commentNum,
|
||||||
|
lastLoginAt: app.lastLoginAt,
|
||||||
|
screen: app.screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
ExploreMiniAppsCompanion toCompanion() => ExploreMiniAppsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
openuid: Value(openuid),
|
||||||
|
devId: Value(devId),
|
||||||
|
icon: Value(icon),
|
||||||
|
iconGaussian: Value(iconGaussian),
|
||||||
|
downloadUrl: Value(downloadUrl),
|
||||||
|
description: Value(description),
|
||||||
|
version: Value(version),
|
||||||
|
typ: Value(typ),
|
||||||
|
flag: Value(flag),
|
||||||
|
reviewStatus: Value(reviewStatus),
|
||||||
|
favoriteAt: Value(favoriteAt),
|
||||||
|
isActive: Value(isActive),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
score: Value(score),
|
||||||
|
channels: Value(channels),
|
||||||
|
devName: Value(devName),
|
||||||
|
pictureGaussian: Value(pictureGaussian),
|
||||||
|
picture: Value(picture),
|
||||||
|
commentNum: Value(commentNum),
|
||||||
|
lastLoginAt: Value(lastLoginAt),
|
||||||
|
screen: Value(screen),
|
||||||
|
);
|
||||||
|
}
|
||||||
202
apps/im_app/lib/data/models/favorite_mini_app_dto.dart
Normal file
202
apps/im_app/lib/data/models/favorite_mini_app_dto.dart
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/favorite_mini_app.dart';
|
||||||
|
|
||||||
|
/// 收藏小程序 DTO
|
||||||
|
class FavoriteMiniAppDto {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const FavoriteMiniAppDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavoriteMiniAppDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
FavoriteMiniAppDto(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'],
|
||||||
|
openuid: json['openuid'],
|
||||||
|
devId: json['dev_id'],
|
||||||
|
icon: json['icon'],
|
||||||
|
iconGaussian: json['icon_gaussian'],
|
||||||
|
downloadUrl: json['download_url'],
|
||||||
|
description: json['description'],
|
||||||
|
version: json['version'],
|
||||||
|
typ: json['typ'],
|
||||||
|
flag: json['flag'],
|
||||||
|
reviewStatus: json['review_status'],
|
||||||
|
favoriteAt: json['favorite_at'],
|
||||||
|
isActive: json['is_active'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
score: json['score']?.toDouble(),
|
||||||
|
channels: json['channels'],
|
||||||
|
devName: json['dev_name'],
|
||||||
|
pictureGaussian: json['picture_gaussian'],
|
||||||
|
picture: json['picture'],
|
||||||
|
commentNum: json['comment_num'],
|
||||||
|
lastLoginAt: json['last_login_at'],
|
||||||
|
screen: json['screen'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'openuid': openuid,
|
||||||
|
'dev_id': devId,
|
||||||
|
'icon': icon,
|
||||||
|
'icon_gaussian': iconGaussian,
|
||||||
|
'download_url': downloadUrl,
|
||||||
|
'description': description,
|
||||||
|
'version': version,
|
||||||
|
'typ': typ,
|
||||||
|
'flag': flag,
|
||||||
|
'review_status': reviewStatus,
|
||||||
|
'favorite_at': favoriteAt,
|
||||||
|
'is_active': isActive,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'score': score,
|
||||||
|
'channels': channels,
|
||||||
|
'dev_name': devName,
|
||||||
|
'picture_gaussian': pictureGaussian,
|
||||||
|
'picture': picture,
|
||||||
|
'comment_num': commentNum,
|
||||||
|
'last_login_at': lastLoginAt,
|
||||||
|
'screen': screen,
|
||||||
|
};
|
||||||
|
|
||||||
|
FavoriteMiniApp toEntity() => FavoriteMiniApp(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
openuid: openuid,
|
||||||
|
devId: devId,
|
||||||
|
icon: icon,
|
||||||
|
iconGaussian: iconGaussian,
|
||||||
|
downloadUrl: downloadUrl,
|
||||||
|
description: description,
|
||||||
|
version: version,
|
||||||
|
typ: typ,
|
||||||
|
flag: flag,
|
||||||
|
reviewStatus: reviewStatus,
|
||||||
|
favoriteAt: favoriteAt,
|
||||||
|
isActive: isActive,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
score: score,
|
||||||
|
channels: channels,
|
||||||
|
devName: devName,
|
||||||
|
pictureGaussian: pictureGaussian,
|
||||||
|
picture: picture,
|
||||||
|
commentNum: commentNum,
|
||||||
|
lastLoginAt: lastLoginAt,
|
||||||
|
screen: screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory FavoriteMiniAppDto.fromEntity(FavoriteMiniApp app) =>
|
||||||
|
FavoriteMiniAppDto(
|
||||||
|
id: app.id,
|
||||||
|
name: app.name,
|
||||||
|
openuid: app.openuid,
|
||||||
|
devId: app.devId,
|
||||||
|
icon: app.icon,
|
||||||
|
iconGaussian: app.iconGaussian,
|
||||||
|
downloadUrl: app.downloadUrl,
|
||||||
|
description: app.description,
|
||||||
|
version: app.version,
|
||||||
|
typ: app.typ,
|
||||||
|
flag: app.flag,
|
||||||
|
reviewStatus: app.reviewStatus,
|
||||||
|
favoriteAt: app.favoriteAt,
|
||||||
|
isActive: app.isActive,
|
||||||
|
createdAt: app.createdAt,
|
||||||
|
updatedAt: app.updatedAt,
|
||||||
|
deletedAt: app.deletedAt,
|
||||||
|
score: app.score,
|
||||||
|
channels: app.channels,
|
||||||
|
devName: app.devName,
|
||||||
|
pictureGaussian: app.pictureGaussian,
|
||||||
|
picture: app.picture,
|
||||||
|
commentNum: app.commentNum,
|
||||||
|
lastLoginAt: app.lastLoginAt,
|
||||||
|
screen: app.screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
FavoriteMiniAppsCompanion toCompanion() => FavoriteMiniAppsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
openuid: Value(openuid),
|
||||||
|
devId: Value(devId),
|
||||||
|
icon: Value(icon),
|
||||||
|
iconGaussian: Value(iconGaussian),
|
||||||
|
downloadUrl: Value(downloadUrl),
|
||||||
|
description: Value(description),
|
||||||
|
version: Value(version),
|
||||||
|
typ: Value(typ),
|
||||||
|
flag: Value(flag),
|
||||||
|
reviewStatus: Value(reviewStatus),
|
||||||
|
favoriteAt: Value(favoriteAt),
|
||||||
|
isActive: Value(isActive),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
score: Value(score),
|
||||||
|
channels: Value(channels),
|
||||||
|
devName: Value(devName),
|
||||||
|
pictureGaussian: Value(pictureGaussian),
|
||||||
|
picture: Value(picture),
|
||||||
|
commentNum: Value(commentNum),
|
||||||
|
lastLoginAt: Value(lastLoginAt),
|
||||||
|
screen: Value(screen),
|
||||||
|
);
|
||||||
|
}
|
||||||
83
apps/im_app/lib/data/models/favourite_detail_dto.dart
Normal file
83
apps/im_app/lib/data/models/favourite_detail_dto.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/favourite_detail.dart';
|
||||||
|
|
||||||
|
/// 收藏详情 DTO
|
||||||
|
class FavouriteDetailDto {
|
||||||
|
final int? id;
|
||||||
|
final String relatedId;
|
||||||
|
final String content;
|
||||||
|
final int? typ;
|
||||||
|
final int? messageId;
|
||||||
|
final int? sendId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? sendTime;
|
||||||
|
|
||||||
|
const FavouriteDetailDto({
|
||||||
|
this.id,
|
||||||
|
this.relatedId = '',
|
||||||
|
this.content = '',
|
||||||
|
this.typ,
|
||||||
|
this.messageId,
|
||||||
|
this.sendId,
|
||||||
|
this.chatId,
|
||||||
|
this.sendTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavouriteDetailDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
FavouriteDetailDto(
|
||||||
|
id: json['id'],
|
||||||
|
relatedId: json['related_id'] ?? '',
|
||||||
|
content: json['content'] ?? '',
|
||||||
|
typ: json['typ'],
|
||||||
|
messageId: json['messageId'],
|
||||||
|
sendId: json['sendId'],
|
||||||
|
chatId: json['chatId'],
|
||||||
|
sendTime: json['sendTime'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'related_id': relatedId,
|
||||||
|
'content': content,
|
||||||
|
'typ': typ,
|
||||||
|
'messageId': messageId,
|
||||||
|
'sendId': sendId,
|
||||||
|
'chatId': chatId,
|
||||||
|
'sendTime': sendTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
FavouriteDetail toEntity() => FavouriteDetail(
|
||||||
|
id: id,
|
||||||
|
relatedId: relatedId,
|
||||||
|
content: content,
|
||||||
|
typ: typ,
|
||||||
|
messageId: messageId,
|
||||||
|
sendId: sendId,
|
||||||
|
chatId: chatId,
|
||||||
|
sendTime: sendTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory FavouriteDetailDto.fromEntity(FavouriteDetail detail) =>
|
||||||
|
FavouriteDetailDto(
|
||||||
|
id: detail.id,
|
||||||
|
relatedId: detail.relatedId,
|
||||||
|
content: detail.content,
|
||||||
|
typ: detail.typ,
|
||||||
|
messageId: detail.messageId,
|
||||||
|
sendId: detail.sendId,
|
||||||
|
chatId: detail.chatId,
|
||||||
|
sendTime: detail.sendTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
FavouriteDetailsCompanion toCompanion() => FavouriteDetailsCompanion(
|
||||||
|
id: id != null ? Value(id!) : const Value.absent(),
|
||||||
|
relatedId: Value(relatedId),
|
||||||
|
content: Value(content),
|
||||||
|
typ: Value(typ),
|
||||||
|
messageId: Value(messageId),
|
||||||
|
sendId: Value(sendId),
|
||||||
|
chatId: Value(chatId),
|
||||||
|
sendTime: Value(sendTime),
|
||||||
|
);
|
||||||
|
}
|
||||||
130
apps/im_app/lib/data/models/favourite_dto.dart
Normal file
130
apps/im_app/lib/data/models/favourite_dto.dart
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/favourite.dart';
|
||||||
|
|
||||||
|
/// 收藏 DTO
|
||||||
|
class FavouriteDto {
|
||||||
|
final int id;
|
||||||
|
final String parentId;
|
||||||
|
final String data;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
final int? source;
|
||||||
|
final int? userId;
|
||||||
|
final int? authorId;
|
||||||
|
final String typ;
|
||||||
|
final String tag;
|
||||||
|
final int isPin;
|
||||||
|
final int chatTyp;
|
||||||
|
final int isUploaded;
|
||||||
|
final String urls;
|
||||||
|
|
||||||
|
const FavouriteDto({
|
||||||
|
required this.id,
|
||||||
|
this.parentId = '',
|
||||||
|
this.data = '',
|
||||||
|
this.createdAt = 0,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
this.source,
|
||||||
|
this.userId,
|
||||||
|
this.authorId,
|
||||||
|
this.typ = '[]',
|
||||||
|
this.tag = '[]',
|
||||||
|
this.isPin = 0,
|
||||||
|
this.chatTyp = 0,
|
||||||
|
this.isUploaded = 1,
|
||||||
|
this.urls = '[]',
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavouriteDto.fromJson(Map<String, dynamic> json) => FavouriteDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
parentId: json['parent_id'] ?? '',
|
||||||
|
data: json['data'] ?? '',
|
||||||
|
createdAt: json['created_at'] ?? 0,
|
||||||
|
updatedAt: json['updated_at'] ?? 0,
|
||||||
|
deletedAt: json['deleted_at'] ?? 0,
|
||||||
|
source: json['source'],
|
||||||
|
userId: json['user_id'],
|
||||||
|
authorId: json['author_id'],
|
||||||
|
typ: json['typ'] ?? '[]',
|
||||||
|
tag: json['tag'] ?? '[]',
|
||||||
|
isPin: json['is_pin'] ?? 0,
|
||||||
|
chatTyp: json['chat_typ'] ?? 0,
|
||||||
|
isUploaded: json['is_uploaded'] ?? 1,
|
||||||
|
urls: json['urls'] ?? '[]',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'parent_id': parentId,
|
||||||
|
'data': data,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'source': source,
|
||||||
|
'user_id': userId,
|
||||||
|
'author_id': authorId,
|
||||||
|
'typ': typ,
|
||||||
|
'tag': tag,
|
||||||
|
'is_pin': isPin,
|
||||||
|
'chat_typ': chatTyp,
|
||||||
|
'is_uploaded': isUploaded,
|
||||||
|
'urls': urls,
|
||||||
|
};
|
||||||
|
|
||||||
|
Favourite toEntity() => Favourite(
|
||||||
|
id: id,
|
||||||
|
parentId: parentId,
|
||||||
|
data: data,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
source: source,
|
||||||
|
userId: userId,
|
||||||
|
authorId: authorId,
|
||||||
|
typ: typ,
|
||||||
|
tag: tag,
|
||||||
|
isPin: isPin,
|
||||||
|
chatTyp: chatTyp,
|
||||||
|
isUploaded: isUploaded,
|
||||||
|
urls: urls,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory FavouriteDto.fromEntity(Favourite favourite) => FavouriteDto(
|
||||||
|
id: favourite.id,
|
||||||
|
parentId: favourite.parentId,
|
||||||
|
data: favourite.data,
|
||||||
|
createdAt: favourite.createdAt,
|
||||||
|
updatedAt: favourite.updatedAt,
|
||||||
|
deletedAt: favourite.deletedAt,
|
||||||
|
source: favourite.source,
|
||||||
|
userId: favourite.userId,
|
||||||
|
authorId: favourite.authorId,
|
||||||
|
typ: favourite.typ,
|
||||||
|
tag: favourite.tag,
|
||||||
|
isPin: favourite.isPin,
|
||||||
|
chatTyp: favourite.chatTyp,
|
||||||
|
isUploaded: favourite.isUploaded,
|
||||||
|
urls: favourite.urls,
|
||||||
|
);
|
||||||
|
|
||||||
|
FavouritesCompanion toCompanion() => FavouritesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
parentId: Value(parentId),
|
||||||
|
data: Value(data),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
source: Value(source),
|
||||||
|
userId: Value(userId),
|
||||||
|
authorId: Value(authorId),
|
||||||
|
typ: Value(typ),
|
||||||
|
tag: Value(tag),
|
||||||
|
isPin: Value(isPin),
|
||||||
|
chatTyp: Value(chatTyp),
|
||||||
|
isUploaded: Value(isUploaded),
|
||||||
|
urls: Value(urls),
|
||||||
|
);
|
||||||
|
}
|
||||||
221
apps/im_app/lib/data/models/group_dto.dart
Normal file
221
apps/im_app/lib/data/models/group_dto.dart
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/group.dart';
|
||||||
|
|
||||||
|
/// 群组 DTO
|
||||||
|
class GroupDto {
|
||||||
|
final int id;
|
||||||
|
final int? userJoinDate;
|
||||||
|
final String? name;
|
||||||
|
final String? profile;
|
||||||
|
final String? icon;
|
||||||
|
final String iconGaussian;
|
||||||
|
final int? permission;
|
||||||
|
final int? admin;
|
||||||
|
final String? members;
|
||||||
|
final int? owner;
|
||||||
|
final String? admins;
|
||||||
|
final int? visible;
|
||||||
|
final int? speakInterval;
|
||||||
|
final int? groupType;
|
||||||
|
final int? roomType;
|
||||||
|
final int? maxNumber;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final int? createTime;
|
||||||
|
final int? updateTime;
|
||||||
|
final int? addIndex;
|
||||||
|
final int? maxMember;
|
||||||
|
final int? expireTime;
|
||||||
|
final int workspaceId;
|
||||||
|
final int mode;
|
||||||
|
final int redpacketPlay;
|
||||||
|
final String? topic;
|
||||||
|
final String? rp;
|
||||||
|
|
||||||
|
const GroupDto({
|
||||||
|
required this.id,
|
||||||
|
this.userJoinDate,
|
||||||
|
this.name,
|
||||||
|
this.profile,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian = '',
|
||||||
|
this.permission,
|
||||||
|
this.admin,
|
||||||
|
this.members,
|
||||||
|
this.owner,
|
||||||
|
this.admins,
|
||||||
|
this.visible,
|
||||||
|
this.speakInterval,
|
||||||
|
this.groupType,
|
||||||
|
this.roomType,
|
||||||
|
this.maxNumber,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.createTime,
|
||||||
|
this.updateTime,
|
||||||
|
this.addIndex,
|
||||||
|
this.maxMember,
|
||||||
|
this.expireTime,
|
||||||
|
this.workspaceId = 0,
|
||||||
|
this.mode = 0,
|
||||||
|
this.redpacketPlay = 0,
|
||||||
|
this.topic,
|
||||||
|
this.rp,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroupDto.fromJson(Map<String, dynamic> json) => GroupDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
userJoinDate: json['user_join_date'],
|
||||||
|
name: json['name'],
|
||||||
|
profile: json['profile'],
|
||||||
|
icon: json['icon'],
|
||||||
|
iconGaussian: json['icon_gaussian'] ?? '',
|
||||||
|
permission: json['permission'],
|
||||||
|
admin: json['admin'],
|
||||||
|
members: json['members'],
|
||||||
|
owner: json['owner'],
|
||||||
|
admins: json['admins'],
|
||||||
|
visible: json['visible'],
|
||||||
|
speakInterval: json['speak_interval'],
|
||||||
|
groupType: json['group_type'],
|
||||||
|
roomType: json['room_type'],
|
||||||
|
maxNumber: json['max_number'],
|
||||||
|
channelId: json['channel_id'],
|
||||||
|
channelGroupId: json['channel_group_id'],
|
||||||
|
createTime: json['create_time'],
|
||||||
|
updateTime: json['update_time'],
|
||||||
|
addIndex: json['__add_index'],
|
||||||
|
maxMember: json['max_member'],
|
||||||
|
expireTime: json['expire_time'],
|
||||||
|
workspaceId: json['workspace_id'] ?? 0,
|
||||||
|
mode: json['mode'] ?? 0,
|
||||||
|
redpacketPlay: json['redpacket_play'] ?? 0,
|
||||||
|
topic: json['topic'],
|
||||||
|
rp: json['rp'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'user_join_date': userJoinDate,
|
||||||
|
'name': name,
|
||||||
|
'profile': profile,
|
||||||
|
'icon': icon,
|
||||||
|
'icon_gaussian': iconGaussian,
|
||||||
|
'permission': permission,
|
||||||
|
'admin': admin,
|
||||||
|
'members': members,
|
||||||
|
'owner': owner,
|
||||||
|
'admins': admins,
|
||||||
|
'visible': visible,
|
||||||
|
'speak_interval': speakInterval,
|
||||||
|
'group_type': groupType,
|
||||||
|
'room_type': roomType,
|
||||||
|
'max_number': maxNumber,
|
||||||
|
'channel_id': channelId,
|
||||||
|
'channel_group_id': channelGroupId,
|
||||||
|
'create_time': createTime,
|
||||||
|
'update_time': updateTime,
|
||||||
|
'__add_index': addIndex,
|
||||||
|
'max_member': maxMember,
|
||||||
|
'expire_time': expireTime,
|
||||||
|
'workspace_id': workspaceId,
|
||||||
|
'mode': mode,
|
||||||
|
'redpacket_play': redpacketPlay,
|
||||||
|
'topic': topic,
|
||||||
|
'rp': rp,
|
||||||
|
};
|
||||||
|
|
||||||
|
Group toEntity() => Group(
|
||||||
|
id: id,
|
||||||
|
userJoinDate: userJoinDate,
|
||||||
|
name: name,
|
||||||
|
profile: profile,
|
||||||
|
icon: icon,
|
||||||
|
iconGaussian: iconGaussian,
|
||||||
|
permission: permission,
|
||||||
|
admin: admin,
|
||||||
|
members: members,
|
||||||
|
owner: owner,
|
||||||
|
admins: admins,
|
||||||
|
visible: visible,
|
||||||
|
speakInterval: speakInterval,
|
||||||
|
groupType: groupType,
|
||||||
|
roomType: roomType,
|
||||||
|
maxNumber: maxNumber,
|
||||||
|
channelId: channelId,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
createTime: createTime,
|
||||||
|
updateTime: updateTime,
|
||||||
|
addIndex: addIndex,
|
||||||
|
maxMember: maxMember,
|
||||||
|
expireTime: expireTime,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
mode: mode,
|
||||||
|
redpacketPlay: redpacketPlay,
|
||||||
|
topic: topic,
|
||||||
|
rp: rp,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory GroupDto.fromEntity(Group group) => GroupDto(
|
||||||
|
id: group.id,
|
||||||
|
userJoinDate: group.userJoinDate,
|
||||||
|
name: group.name,
|
||||||
|
profile: group.profile,
|
||||||
|
icon: group.icon,
|
||||||
|
iconGaussian: group.iconGaussian,
|
||||||
|
permission: group.permission,
|
||||||
|
admin: group.admin,
|
||||||
|
members: group.members,
|
||||||
|
owner: group.owner,
|
||||||
|
admins: group.admins,
|
||||||
|
visible: group.visible,
|
||||||
|
speakInterval: group.speakInterval,
|
||||||
|
groupType: group.groupType,
|
||||||
|
roomType: group.roomType,
|
||||||
|
maxNumber: group.maxNumber,
|
||||||
|
channelId: group.channelId,
|
||||||
|
channelGroupId: group.channelGroupId,
|
||||||
|
createTime: group.createTime,
|
||||||
|
updateTime: group.updateTime,
|
||||||
|
addIndex: group.addIndex,
|
||||||
|
maxMember: group.maxMember,
|
||||||
|
expireTime: group.expireTime,
|
||||||
|
workspaceId: group.workspaceId,
|
||||||
|
mode: group.mode,
|
||||||
|
redpacketPlay: group.redpacketPlay,
|
||||||
|
topic: group.topic,
|
||||||
|
rp: group.rp,
|
||||||
|
);
|
||||||
|
|
||||||
|
GroupsCompanion toCompanion() => GroupsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
userJoinDate: Value(userJoinDate),
|
||||||
|
name: Value(name),
|
||||||
|
profile: Value(profile),
|
||||||
|
icon: Value(icon),
|
||||||
|
iconGaussian: Value(iconGaussian),
|
||||||
|
permission: Value(permission),
|
||||||
|
admin: Value(admin),
|
||||||
|
members: Value(members),
|
||||||
|
owner: Value(owner),
|
||||||
|
admins: Value(admins),
|
||||||
|
visible: Value(visible),
|
||||||
|
speakInterval: Value(speakInterval),
|
||||||
|
groupType: Value(groupType),
|
||||||
|
roomType: Value(roomType),
|
||||||
|
maxNumber: Value(maxNumber),
|
||||||
|
channelId: Value(channelId),
|
||||||
|
channelGroupId: Value(channelGroupId),
|
||||||
|
createTime: Value(createTime),
|
||||||
|
updateTime: Value(updateTime),
|
||||||
|
addIndex: Value(addIndex),
|
||||||
|
maxMember: Value(maxMember),
|
||||||
|
expireTime: Value(expireTime),
|
||||||
|
workspaceId: Value(workspaceId),
|
||||||
|
mode: Value(mode),
|
||||||
|
redpacketPlay: Value(redpacketPlay),
|
||||||
|
topic: Value(topic),
|
||||||
|
rp: Value(rp),
|
||||||
|
);
|
||||||
|
}
|
||||||
137
apps/im_app/lib/data/models/message_dto.dart
Normal file
137
apps/im_app/lib/data/models/message_dto.dart
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/message.dart';
|
||||||
|
|
||||||
|
/// 消息 DTO
|
||||||
|
class MessageDto {
|
||||||
|
final int id;
|
||||||
|
final int? messageId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? chatIdx;
|
||||||
|
final int? sendId;
|
||||||
|
final String? content;
|
||||||
|
final int? typ;
|
||||||
|
final int? sendTime;
|
||||||
|
final int? expireTime;
|
||||||
|
final int? createTime;
|
||||||
|
final String? atUsers;
|
||||||
|
final String emojis;
|
||||||
|
final int editTime;
|
||||||
|
final int refTyp;
|
||||||
|
final int flag;
|
||||||
|
final String cmid;
|
||||||
|
|
||||||
|
const MessageDto({
|
||||||
|
required this.id,
|
||||||
|
this.messageId,
|
||||||
|
this.chatId,
|
||||||
|
this.chatIdx,
|
||||||
|
this.sendId,
|
||||||
|
this.content,
|
||||||
|
this.typ,
|
||||||
|
this.sendTime,
|
||||||
|
this.expireTime,
|
||||||
|
this.createTime,
|
||||||
|
this.atUsers,
|
||||||
|
this.emojis = '[]',
|
||||||
|
this.editTime = 0,
|
||||||
|
this.refTyp = 0,
|
||||||
|
this.flag = 0,
|
||||||
|
this.cmid = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MessageDto.fromJson(Map<String, dynamic> json) => MessageDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
messageId: json['message_id'],
|
||||||
|
chatId: json['chat_id'],
|
||||||
|
chatIdx: json['chat_idx'],
|
||||||
|
sendId: json['send_id'],
|
||||||
|
content: json['content'],
|
||||||
|
typ: json['typ'],
|
||||||
|
sendTime: json['send_time'],
|
||||||
|
expireTime: json['expire_time'],
|
||||||
|
createTime: json['create_time'],
|
||||||
|
atUsers: json['at_users'],
|
||||||
|
emojis: json['emojis'] ?? '[]',
|
||||||
|
editTime: json['edit_time'] ?? 0,
|
||||||
|
refTyp: json['ref_typ'] ?? 0,
|
||||||
|
flag: json['flag'] ?? 0,
|
||||||
|
cmid: json['cmid'] ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'message_id': messageId,
|
||||||
|
'chat_id': chatId,
|
||||||
|
'chat_idx': chatIdx,
|
||||||
|
'send_id': sendId,
|
||||||
|
'content': content,
|
||||||
|
'typ': typ,
|
||||||
|
'send_time': sendTime,
|
||||||
|
'expire_time': expireTime,
|
||||||
|
'create_time': createTime,
|
||||||
|
'at_users': atUsers,
|
||||||
|
'emojis': emojis,
|
||||||
|
'edit_time': editTime,
|
||||||
|
'ref_typ': refTyp,
|
||||||
|
'flag': flag,
|
||||||
|
'cmid': cmid,
|
||||||
|
};
|
||||||
|
|
||||||
|
Message toEntity() => Message(
|
||||||
|
id: id,
|
||||||
|
messageId: messageId,
|
||||||
|
chatId: chatId,
|
||||||
|
chatIdx: chatIdx,
|
||||||
|
sendId: sendId,
|
||||||
|
content: content,
|
||||||
|
typ: typ,
|
||||||
|
sendTime: sendTime,
|
||||||
|
expireTime: expireTime,
|
||||||
|
createTime: createTime,
|
||||||
|
atUsers: atUsers,
|
||||||
|
emojis: emojis,
|
||||||
|
editTime: editTime,
|
||||||
|
refTyp: refTyp,
|
||||||
|
flag: flag,
|
||||||
|
cmid: cmid,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory MessageDto.fromEntity(Message message) => MessageDto(
|
||||||
|
id: message.id,
|
||||||
|
messageId: message.messageId,
|
||||||
|
chatId: message.chatId,
|
||||||
|
chatIdx: message.chatIdx,
|
||||||
|
sendId: message.sendId,
|
||||||
|
content: message.content,
|
||||||
|
typ: message.typ,
|
||||||
|
sendTime: message.sendTime,
|
||||||
|
expireTime: message.expireTime,
|
||||||
|
createTime: message.createTime,
|
||||||
|
atUsers: message.atUsers,
|
||||||
|
emojis: message.emojis,
|
||||||
|
editTime: message.editTime,
|
||||||
|
refTyp: message.refTyp,
|
||||||
|
flag: message.flag,
|
||||||
|
cmid: message.cmid,
|
||||||
|
);
|
||||||
|
|
||||||
|
MessagesCompanion toCompanion() => MessagesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
messageId: Value(messageId),
|
||||||
|
chatId: Value(chatId),
|
||||||
|
chatIdx: Value(chatIdx),
|
||||||
|
sendId: Value(sendId),
|
||||||
|
content: Value(content),
|
||||||
|
typ: Value(typ),
|
||||||
|
sendTime: Value(sendTime),
|
||||||
|
expireTime: Value(expireTime),
|
||||||
|
createTime: Value(createTime),
|
||||||
|
atUsers: Value(atUsers),
|
||||||
|
emojis: Value(emojis),
|
||||||
|
editTime: Value(editTime),
|
||||||
|
refTyp: Value(refTyp),
|
||||||
|
flag: Value(flag),
|
||||||
|
cmid: Value(cmid),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/pending_friend_request_history.dart';
|
||||||
|
|
||||||
|
/// 待处理好友请求历史 DTO
|
||||||
|
class PendingFriendRequestHistoryDto {
|
||||||
|
final int id;
|
||||||
|
final int uid;
|
||||||
|
final int requestTime;
|
||||||
|
final String? remarks;
|
||||||
|
final String? source;
|
||||||
|
final int? rs;
|
||||||
|
|
||||||
|
const PendingFriendRequestHistoryDto({
|
||||||
|
required this.id,
|
||||||
|
required this.uid,
|
||||||
|
required this.requestTime,
|
||||||
|
this.remarks,
|
||||||
|
this.source,
|
||||||
|
this.rs,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PendingFriendRequestHistoryDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
PendingFriendRequestHistoryDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
uid: json['uid'] as int,
|
||||||
|
requestTime: json['request_time'] as int,
|
||||||
|
remarks: json['remarks'],
|
||||||
|
source: json['source'],
|
||||||
|
rs: json['rs'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'uid': uid,
|
||||||
|
'request_time': requestTime,
|
||||||
|
'remarks': remarks,
|
||||||
|
'source': source,
|
||||||
|
'rs': rs,
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingFriendRequestHistory toEntity() => PendingFriendRequestHistory(
|
||||||
|
id: id,
|
||||||
|
uid: uid,
|
||||||
|
requestTime: requestTime,
|
||||||
|
remarks: remarks,
|
||||||
|
source: source,
|
||||||
|
rs: rs,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory PendingFriendRequestHistoryDto.fromEntity(
|
||||||
|
PendingFriendRequestHistory history,
|
||||||
|
) => PendingFriendRequestHistoryDto(
|
||||||
|
id: history.id,
|
||||||
|
uid: history.uid,
|
||||||
|
requestTime: history.requestTime,
|
||||||
|
remarks: history.remarks,
|
||||||
|
source: history.source,
|
||||||
|
rs: history.rs,
|
||||||
|
);
|
||||||
|
|
||||||
|
PendingFriendRequestHistoriesCompanion toCompanion() =>
|
||||||
|
PendingFriendRequestHistoriesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
uid: Value(uid),
|
||||||
|
requestTime: Value(requestTime),
|
||||||
|
remarks: Value(remarks),
|
||||||
|
source: Value(source),
|
||||||
|
rs: Value(rs),
|
||||||
|
);
|
||||||
|
}
|
||||||
201
apps/im_app/lib/data/models/recent_mini_app_dto.dart
Normal file
201
apps/im_app/lib/data/models/recent_mini_app_dto.dart
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/recent_mini_app.dart';
|
||||||
|
|
||||||
|
/// 最近小程序 DTO
|
||||||
|
class RecentMiniAppDto {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const RecentMiniAppDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecentMiniAppDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
RecentMiniAppDto(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'],
|
||||||
|
openuid: json['openuid'],
|
||||||
|
devId: json['dev_id'],
|
||||||
|
icon: json['icon'],
|
||||||
|
iconGaussian: json['icon_gaussian'],
|
||||||
|
downloadUrl: json['download_url'],
|
||||||
|
description: json['description'],
|
||||||
|
version: json['version'],
|
||||||
|
typ: json['typ'],
|
||||||
|
flag: json['flag'],
|
||||||
|
reviewStatus: json['review_status'],
|
||||||
|
favoriteAt: json['favorite_at'],
|
||||||
|
isActive: json['is_active'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
score: json['score']?.toDouble(),
|
||||||
|
channels: json['channels'],
|
||||||
|
devName: json['dev_name'],
|
||||||
|
pictureGaussian: json['picture_gaussian'],
|
||||||
|
picture: json['picture'],
|
||||||
|
commentNum: json['comment_num'],
|
||||||
|
lastLoginAt: json['last_login_at'],
|
||||||
|
screen: json['screen'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'openuid': openuid,
|
||||||
|
'dev_id': devId,
|
||||||
|
'icon': icon,
|
||||||
|
'icon_gaussian': iconGaussian,
|
||||||
|
'download_url': downloadUrl,
|
||||||
|
'description': description,
|
||||||
|
'version': version,
|
||||||
|
'typ': typ,
|
||||||
|
'flag': flag,
|
||||||
|
'review_status': reviewStatus,
|
||||||
|
'favorite_at': favoriteAt,
|
||||||
|
'is_active': isActive,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'score': score,
|
||||||
|
'channels': channels,
|
||||||
|
'dev_name': devName,
|
||||||
|
'picture_gaussian': pictureGaussian,
|
||||||
|
'picture': picture,
|
||||||
|
'comment_num': commentNum,
|
||||||
|
'last_login_at': lastLoginAt,
|
||||||
|
'screen': screen,
|
||||||
|
};
|
||||||
|
|
||||||
|
RecentMiniApp toEntity() => RecentMiniApp(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
openuid: openuid,
|
||||||
|
devId: devId,
|
||||||
|
icon: icon,
|
||||||
|
iconGaussian: iconGaussian,
|
||||||
|
downloadUrl: downloadUrl,
|
||||||
|
description: description,
|
||||||
|
version: version,
|
||||||
|
typ: typ,
|
||||||
|
flag: flag,
|
||||||
|
reviewStatus: reviewStatus,
|
||||||
|
favoriteAt: favoriteAt,
|
||||||
|
isActive: isActive,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
score: score,
|
||||||
|
channels: channels,
|
||||||
|
devName: devName,
|
||||||
|
pictureGaussian: pictureGaussian,
|
||||||
|
picture: picture,
|
||||||
|
commentNum: commentNum,
|
||||||
|
lastLoginAt: lastLoginAt,
|
||||||
|
screen: screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory RecentMiniAppDto.fromEntity(RecentMiniApp app) => RecentMiniAppDto(
|
||||||
|
id: app.id,
|
||||||
|
name: app.name,
|
||||||
|
openuid: app.openuid,
|
||||||
|
devId: app.devId,
|
||||||
|
icon: app.icon,
|
||||||
|
iconGaussian: app.iconGaussian,
|
||||||
|
downloadUrl: app.downloadUrl,
|
||||||
|
description: app.description,
|
||||||
|
version: app.version,
|
||||||
|
typ: app.typ,
|
||||||
|
flag: app.flag,
|
||||||
|
reviewStatus: app.reviewStatus,
|
||||||
|
favoriteAt: app.favoriteAt,
|
||||||
|
isActive: app.isActive,
|
||||||
|
createdAt: app.createdAt,
|
||||||
|
updatedAt: app.updatedAt,
|
||||||
|
deletedAt: app.deletedAt,
|
||||||
|
score: app.score,
|
||||||
|
channels: app.channels,
|
||||||
|
devName: app.devName,
|
||||||
|
pictureGaussian: app.pictureGaussian,
|
||||||
|
picture: app.picture,
|
||||||
|
commentNum: app.commentNum,
|
||||||
|
lastLoginAt: app.lastLoginAt,
|
||||||
|
screen: app.screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
RecentMiniAppsCompanion toCompanion() => RecentMiniAppsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
openuid: Value(openuid),
|
||||||
|
devId: Value(devId),
|
||||||
|
icon: Value(icon),
|
||||||
|
iconGaussian: Value(iconGaussian),
|
||||||
|
downloadUrl: Value(downloadUrl),
|
||||||
|
description: Value(description),
|
||||||
|
version: Value(version),
|
||||||
|
typ: Value(typ),
|
||||||
|
flag: Value(flag),
|
||||||
|
reviewStatus: Value(reviewStatus),
|
||||||
|
favoriteAt: Value(favoriteAt),
|
||||||
|
isActive: Value(isActive),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
score: Value(score),
|
||||||
|
channels: Value(channels),
|
||||||
|
devName: Value(devName),
|
||||||
|
pictureGaussian: Value(pictureGaussian),
|
||||||
|
picture: Value(picture),
|
||||||
|
commentNum: Value(commentNum),
|
||||||
|
lastLoginAt: Value(lastLoginAt),
|
||||||
|
screen: Value(screen),
|
||||||
|
);
|
||||||
|
}
|
||||||
109
apps/im_app/lib/data/models/retry_dto.dart
Normal file
109
apps/im_app/lib/data/models/retry_dto.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/retry.dart';
|
||||||
|
|
||||||
|
/// 重试 DTO
|
||||||
|
class RetryDto {
|
||||||
|
final int? id;
|
||||||
|
final int? uid;
|
||||||
|
final String apiType;
|
||||||
|
final String endPoint;
|
||||||
|
final String requestData;
|
||||||
|
final int? synced;
|
||||||
|
final String callbackFun;
|
||||||
|
final int? expired;
|
||||||
|
final int? replace;
|
||||||
|
final int? expireTime;
|
||||||
|
final int? createTime;
|
||||||
|
final int? addIndex;
|
||||||
|
|
||||||
|
const RetryDto({
|
||||||
|
this.id,
|
||||||
|
this.uid,
|
||||||
|
this.apiType = '',
|
||||||
|
this.endPoint = '',
|
||||||
|
this.requestData = '',
|
||||||
|
this.synced,
|
||||||
|
this.callbackFun = '',
|
||||||
|
this.expired,
|
||||||
|
this.replace,
|
||||||
|
this.expireTime,
|
||||||
|
this.createTime,
|
||||||
|
this.addIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RetryDto.fromJson(Map<String, dynamic> json) => RetryDto(
|
||||||
|
id: json['id'],
|
||||||
|
uid: json['uid'],
|
||||||
|
apiType: json['api_type'] ?? '',
|
||||||
|
endPoint: json['end_point'] ?? '',
|
||||||
|
requestData: json['request_data'] ?? '',
|
||||||
|
synced: json['synced'],
|
||||||
|
callbackFun: json['callback_fun'] ?? '',
|
||||||
|
expired: json['expired'],
|
||||||
|
replace: json['replace'],
|
||||||
|
expireTime: json['expire_time'],
|
||||||
|
createTime: json['create_time'],
|
||||||
|
addIndex: json['__add_index'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'uid': uid,
|
||||||
|
'api_type': apiType,
|
||||||
|
'end_point': endPoint,
|
||||||
|
'request_data': requestData,
|
||||||
|
'synced': synced,
|
||||||
|
'callback_fun': callbackFun,
|
||||||
|
'expired': expired,
|
||||||
|
'replace': replace,
|
||||||
|
'expire_time': expireTime,
|
||||||
|
'create_time': createTime,
|
||||||
|
'__add_index': addIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
Retry toEntity() => Retry(
|
||||||
|
id: id,
|
||||||
|
uid: uid,
|
||||||
|
apiType: apiType,
|
||||||
|
endPoint: endPoint,
|
||||||
|
requestData: requestData,
|
||||||
|
synced: synced,
|
||||||
|
callbackFun: callbackFun,
|
||||||
|
expired: expired,
|
||||||
|
replace: replace,
|
||||||
|
expireTime: expireTime,
|
||||||
|
createTime: createTime,
|
||||||
|
addIndex: addIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory RetryDto.fromEntity(Retry retry) => RetryDto(
|
||||||
|
id: retry.id,
|
||||||
|
uid: retry.uid,
|
||||||
|
apiType: retry.apiType,
|
||||||
|
endPoint: retry.endPoint,
|
||||||
|
requestData: retry.requestData,
|
||||||
|
synced: retry.synced,
|
||||||
|
callbackFun: retry.callbackFun,
|
||||||
|
expired: retry.expired,
|
||||||
|
replace: retry.replace,
|
||||||
|
expireTime: retry.expireTime,
|
||||||
|
createTime: retry.createTime,
|
||||||
|
addIndex: retry.addIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
RetriesCompanion toCompanion() => RetriesCompanion(
|
||||||
|
id: id != null ? Value(id!) : const Value.absent(),
|
||||||
|
uid: Value(uid),
|
||||||
|
apiType: Value(apiType),
|
||||||
|
endPoint: Value(endPoint),
|
||||||
|
requestData: Value(requestData),
|
||||||
|
synced: Value(synced),
|
||||||
|
callbackFun: Value(callbackFun),
|
||||||
|
expired: Value(expired),
|
||||||
|
replace: Value(replace),
|
||||||
|
expireTime: Value(expireTime),
|
||||||
|
createTime: Value(createTime),
|
||||||
|
addIndex: Value(addIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
88
apps/im_app/lib/data/models/sound_dto.dart
Normal file
88
apps/im_app/lib/data/models/sound_dto.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/sound.dart';
|
||||||
|
|
||||||
|
/// 音效 DTO
|
||||||
|
class SoundDto {
|
||||||
|
final int id;
|
||||||
|
final String filePath;
|
||||||
|
final int typ;
|
||||||
|
final String name;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
final int channelGroupId;
|
||||||
|
final int isDefault;
|
||||||
|
|
||||||
|
const SoundDto({
|
||||||
|
required this.id,
|
||||||
|
this.filePath = '',
|
||||||
|
required this.typ,
|
||||||
|
this.name = '',
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
required this.channelGroupId,
|
||||||
|
required this.isDefault,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SoundDto.fromJson(Map<String, dynamic> json) => SoundDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
filePath: json['file_path'] ?? '',
|
||||||
|
typ: json['typ'] as int,
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
createdAt: json['created_at'] as int,
|
||||||
|
updatedAt: json['updated_at'] as int,
|
||||||
|
deletedAt: json['deleted_at'] ?? 0,
|
||||||
|
channelGroupId: json['channel_group_id'] as int,
|
||||||
|
isDefault: json['is_default'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'file_path': filePath,
|
||||||
|
'typ': typ,
|
||||||
|
'name': name,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'channel_group_id': channelGroupId,
|
||||||
|
'is_default': isDefault,
|
||||||
|
};
|
||||||
|
|
||||||
|
Sound toEntity() => Sound(
|
||||||
|
id: id,
|
||||||
|
filePath: filePath,
|
||||||
|
typ: typ,
|
||||||
|
name: name,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
isDefault: isDefault,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory SoundDto.fromEntity(Sound sound) => SoundDto(
|
||||||
|
id: sound.id,
|
||||||
|
filePath: sound.filePath,
|
||||||
|
typ: sound.typ,
|
||||||
|
name: sound.name,
|
||||||
|
createdAt: sound.createdAt,
|
||||||
|
updatedAt: sound.updatedAt,
|
||||||
|
deletedAt: sound.deletedAt,
|
||||||
|
channelGroupId: sound.channelGroupId,
|
||||||
|
isDefault: sound.isDefault,
|
||||||
|
);
|
||||||
|
|
||||||
|
SoundsCompanion toCompanion() => SoundsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
filePath: Value(filePath),
|
||||||
|
typ: Value(typ),
|
||||||
|
name: Value(name),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
channelGroupId: Value(channelGroupId),
|
||||||
|
isDefault: Value(isDefault),
|
||||||
|
);
|
||||||
|
}
|
||||||
74
apps/im_app/lib/data/models/tag_dto.dart
Normal file
74
apps/im_app/lib/data/models/tag_dto.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/tag.dart';
|
||||||
|
|
||||||
|
/// 标签 DTO
|
||||||
|
class TagDto {
|
||||||
|
final int? id;
|
||||||
|
final int? uid;
|
||||||
|
final String name;
|
||||||
|
final int? type;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? addIndex;
|
||||||
|
|
||||||
|
const TagDto({
|
||||||
|
this.id,
|
||||||
|
this.uid,
|
||||||
|
this.name = '',
|
||||||
|
this.type,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.addIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TagDto.fromJson(Map<String, dynamic> json) => TagDto(
|
||||||
|
id: json['id'],
|
||||||
|
uid: json['uid'],
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
type: json['type'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
addIndex: json['__add_index'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'uid': uid,
|
||||||
|
'name': name,
|
||||||
|
'type': type,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'__add_index': addIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
Tag toEntity() => Tag(
|
||||||
|
id: id,
|
||||||
|
uid: uid,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
addIndex: addIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory TagDto.fromEntity(Tag tag) => TagDto(
|
||||||
|
id: tag.id,
|
||||||
|
uid: tag.uid,
|
||||||
|
name: tag.name,
|
||||||
|
type: tag.type,
|
||||||
|
createdAt: tag.createdAt,
|
||||||
|
updatedAt: tag.updatedAt,
|
||||||
|
addIndex: tag.addIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
TagsCompanion toCompanion() => TagsCompanion(
|
||||||
|
id: id != null ? Value(id!) : const Value.absent(),
|
||||||
|
uid: Value(uid),
|
||||||
|
name: Value(name),
|
||||||
|
type: Value(type),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
addIndex: Value(addIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,68 +1,148 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
import '../../domain/entities/user.dart';
|
import 'package:im_app/domain/entities/user.dart';
|
||||||
|
|
||||||
part 'user_dto.g.dart';
|
|
||||||
|
|
||||||
/// 用户 DTO(Data Transfer Object)
|
/// 用户 DTO(Data Transfer Object)
|
||||||
///
|
///
|
||||||
/// local / remote 共用的数据传输对象,放在 data/models/。
|
/// local / remote 共用的数据传输对象,放在 data/models/。
|
||||||
/// 提供与 Domain Entity [User] 之间的双向转换。
|
/// 提供与 Domain Entity [User] 之间的双向转换。
|
||||||
///
|
|
||||||
/// ## 数据流位置(本地存储场景)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// 写入本地:
|
|
||||||
/// LoginData.toEntity() → User
|
|
||||||
/// → UserDto.fromEntity(user) → ★ UserDto ★ ← 你在这里
|
|
||||||
/// → toJson() → SQLite / SharedPreferences
|
|
||||||
///
|
|
||||||
/// 读取本地:
|
|
||||||
/// SQLite / SharedPreferences → JSON
|
|
||||||
/// → ★ UserDto.fromJson() ★ ← 你在这里
|
|
||||||
/// → UserDto.toEntity() → User
|
|
||||||
/// → ViewModel.state → View
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// 注意:登录接口的 Response DTO 是 [LoginData](含 token),
|
|
||||||
/// 本类用于纯用户信息的本地持久化,不含 token。
|
|
||||||
@JsonSerializable()
|
|
||||||
class UserDto {
|
class UserDto {
|
||||||
@JsonKey(name: 'user_id')
|
final int uid;
|
||||||
final String userId;
|
final String? uuid;
|
||||||
final String email;
|
final int? lastOnline;
|
||||||
|
final String? profilePic;
|
||||||
|
final String? profilePicGaussian;
|
||||||
final String? nickname;
|
final String? nickname;
|
||||||
final String? avatar;
|
final String? contact;
|
||||||
|
final String? countryCode;
|
||||||
|
final String? email;
|
||||||
|
final String? recoveryEmail;
|
||||||
|
final String? username;
|
||||||
|
final String? bio;
|
||||||
|
final int? relationship;
|
||||||
|
final String? userAlias;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
const UserDto({
|
const UserDto({
|
||||||
required this.userId,
|
required this.uid,
|
||||||
required this.email,
|
this.uuid,
|
||||||
|
this.lastOnline,
|
||||||
|
this.profilePic,
|
||||||
|
this.profilePicGaussian,
|
||||||
this.nickname,
|
this.nickname,
|
||||||
this.avatar,
|
this.contact,
|
||||||
|
this.countryCode,
|
||||||
|
this.email,
|
||||||
|
this.recoveryEmail,
|
||||||
|
this.username,
|
||||||
|
this.bio,
|
||||||
|
this.relationship,
|
||||||
|
this.userAlias,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.hint,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory UserDto.fromJson(Map<String, dynamic> json) =>
|
factory UserDto.fromJson(Map<String, dynamic> json) => UserDto(
|
||||||
_$UserDtoFromJson(json);
|
uid: json['uid'] as int,
|
||||||
|
uuid: json['uuid'],
|
||||||
|
lastOnline: json['last_online'],
|
||||||
|
profilePic: json['profile_pic'],
|
||||||
|
profilePicGaussian: json['profile_pic_gaussian'] ?? '',
|
||||||
|
nickname: json['nickname'],
|
||||||
|
contact: json['contact'],
|
||||||
|
countryCode: json['country_code'],
|
||||||
|
email: json['email'],
|
||||||
|
recoveryEmail: json['recovery_email'] ?? '',
|
||||||
|
username: json['username'],
|
||||||
|
bio: json['bio'] ?? '',
|
||||||
|
relationship: json['relationship'],
|
||||||
|
userAlias: json['user_alias'],
|
||||||
|
channelId: json['channel_id'],
|
||||||
|
channelGroupId: json['channel_group_id'],
|
||||||
|
hint: json['hint'],
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$UserDtoToJson(this);
|
Map<String, dynamic> toJson() => {
|
||||||
|
'uid': uid,
|
||||||
|
'uuid': uuid,
|
||||||
|
'last_online': lastOnline,
|
||||||
|
'profile_pic': profilePic,
|
||||||
|
'profile_pic_gaussian': profilePicGaussian,
|
||||||
|
'nickname': nickname,
|
||||||
|
'contact': contact,
|
||||||
|
'country_code': countryCode,
|
||||||
|
'email': email,
|
||||||
|
'recovery_email': recoveryEmail,
|
||||||
|
'username': username,
|
||||||
|
'bio': bio,
|
||||||
|
'relationship': relationship,
|
||||||
|
'user_alias': userAlias,
|
||||||
|
'channel_id': channelId,
|
||||||
|
'channel_group_id': channelGroupId,
|
||||||
|
'hint': hint,
|
||||||
|
};
|
||||||
|
|
||||||
/// DTO → Domain Entity
|
/// DTO → Domain Entity
|
||||||
User toEntity() {
|
User toEntity() => User(
|
||||||
return User(
|
uid: uid,
|
||||||
id: userId,
|
uuid: uuid,
|
||||||
email: email,
|
lastOnline: lastOnline,
|
||||||
|
profilePic: profilePic,
|
||||||
|
profilePicGaussian: profilePicGaussian,
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
avatar: avatar,
|
contact: contact,
|
||||||
|
countryCode: countryCode,
|
||||||
|
email: email,
|
||||||
|
recoveryEmail: recoveryEmail,
|
||||||
|
username: username,
|
||||||
|
bio: bio,
|
||||||
|
relationship: relationship,
|
||||||
|
userAlias: userAlias,
|
||||||
|
channelId: channelId,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
hint: hint,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Domain Entity → DTO
|
/// Domain Entity → DTO
|
||||||
factory UserDto.fromEntity(User user) {
|
factory UserDto.fromEntity(User user) => UserDto(
|
||||||
return UserDto(
|
uid: user.uid,
|
||||||
userId: user.id,
|
uuid: user.uuid,
|
||||||
email: user.email,
|
lastOnline: user.lastOnline,
|
||||||
|
profilePic: user.profilePic,
|
||||||
|
profilePicGaussian: user.profilePicGaussian,
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
avatar: user.avatar,
|
contact: user.contact,
|
||||||
|
countryCode: user.countryCode,
|
||||||
|
email: user.email,
|
||||||
|
recoveryEmail: user.recoveryEmail,
|
||||||
|
username: user.username,
|
||||||
|
bio: user.bio,
|
||||||
|
relationship: user.relationship,
|
||||||
|
userAlias: user.userAlias,
|
||||||
|
channelId: user.channelId,
|
||||||
|
channelGroupId: user.channelGroupId,
|
||||||
|
hint: user.hint,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// DTO → Drift Companion (for DB insert/update)
|
||||||
|
UsersCompanion toCompanion() => UsersCompanion(
|
||||||
|
uid: Value(uid),
|
||||||
|
uuid: Value(uuid),
|
||||||
|
lastOnline: Value(lastOnline),
|
||||||
|
profilePic: Value(profilePic),
|
||||||
|
profilePicGaussian: Value(profilePicGaussian ?? ''),
|
||||||
|
nickname: Value(nickname),
|
||||||
|
contact: Value(contact),
|
||||||
|
countryCode: Value(countryCode),
|
||||||
|
email: Value(email),
|
||||||
|
recoveryEmail: Value(recoveryEmail),
|
||||||
|
username: Value(username),
|
||||||
|
bio: Value(bio),
|
||||||
|
relationship: Value(relationship),
|
||||||
|
userAlias: Value(userAlias),
|
||||||
|
hint: Value(hint),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
41
apps/im_app/lib/data/models/user_request_history_dto.dart
Normal file
41
apps/im_app/lib/data/models/user_request_history_dto.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/user_request_history.dart';
|
||||||
|
|
||||||
|
/// 用户请求历史 DTO
|
||||||
|
class UserRequestHistoryDto {
|
||||||
|
final int id;
|
||||||
|
final int? status;
|
||||||
|
final int? createdAt;
|
||||||
|
|
||||||
|
const UserRequestHistoryDto({required this.id, this.status, this.createdAt});
|
||||||
|
|
||||||
|
factory UserRequestHistoryDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
UserRequestHistoryDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
status: json['status'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'status': status,
|
||||||
|
'created_at': createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
UserRequestHistory toEntity() =>
|
||||||
|
UserRequestHistory(id: id, status: status, createdAt: createdAt);
|
||||||
|
|
||||||
|
factory UserRequestHistoryDto.fromEntity(UserRequestHistory history) =>
|
||||||
|
UserRequestHistoryDto(
|
||||||
|
id: history.id,
|
||||||
|
status: history.status,
|
||||||
|
createdAt: history.createdAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
UserRequestHistoriesCompanion toCompanion() => UserRequestHistoriesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
status: Value(status),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
116
apps/im_app/lib/data/models/workspace_dto.dart
Normal file
116
apps/im_app/lib/data/models/workspace_dto.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/workspace.dart';
|
||||||
|
|
||||||
|
/// 工作空间 DTO
|
||||||
|
class WorkspaceDto {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final int? ownerId;
|
||||||
|
final String? description;
|
||||||
|
final String? logo;
|
||||||
|
final int? grade;
|
||||||
|
final int? cap;
|
||||||
|
final String? currency;
|
||||||
|
final int? status;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final int? channelGroupId;
|
||||||
|
|
||||||
|
const WorkspaceDto({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.ownerId,
|
||||||
|
this.description,
|
||||||
|
this.logo,
|
||||||
|
this.grade,
|
||||||
|
this.cap,
|
||||||
|
this.currency,
|
||||||
|
this.status,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.channelGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WorkspaceDto.fromJson(Map<String, dynamic> json) => WorkspaceDto(
|
||||||
|
id: json['id'] as int,
|
||||||
|
name: json['name'],
|
||||||
|
ownerId: json['owner_id'],
|
||||||
|
description: json['description'],
|
||||||
|
logo: json['logo'],
|
||||||
|
grade: json['grade'],
|
||||||
|
cap: json['cap'],
|
||||||
|
currency: json['currency'],
|
||||||
|
status: json['status'],
|
||||||
|
createdAt: json['created_at'],
|
||||||
|
updatedAt: json['updated_at'],
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
channelGroupId: json['channel_group_id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'owner_id': ownerId,
|
||||||
|
'description': description,
|
||||||
|
'logo': logo,
|
||||||
|
'grade': grade,
|
||||||
|
'cap': cap,
|
||||||
|
'currency': currency,
|
||||||
|
'status': status,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
'deleted_at': deletedAt,
|
||||||
|
'channel_group_id': channelGroupId,
|
||||||
|
};
|
||||||
|
|
||||||
|
Workspace toEntity() => Workspace(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
ownerId: ownerId,
|
||||||
|
description: description,
|
||||||
|
logo: logo,
|
||||||
|
grade: grade,
|
||||||
|
cap: cap,
|
||||||
|
currency: currency,
|
||||||
|
status: status,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory WorkspaceDto.fromEntity(Workspace workspace) => WorkspaceDto(
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
description: workspace.description,
|
||||||
|
logo: workspace.logo,
|
||||||
|
grade: workspace.grade,
|
||||||
|
cap: workspace.cap,
|
||||||
|
currency: workspace.currency,
|
||||||
|
status: workspace.status,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
updatedAt: workspace.updatedAt,
|
||||||
|
deletedAt: workspace.deletedAt,
|
||||||
|
channelGroupId: workspace.channelGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkspacesCompanion toCompanion() => WorkspacesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
ownerId: Value(ownerId),
|
||||||
|
description: Value(description),
|
||||||
|
logo: Value(logo),
|
||||||
|
grade: Value(grade),
|
||||||
|
cap: Value(cap),
|
||||||
|
currency: Value(currency),
|
||||||
|
status: Value(status),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(updatedAt),
|
||||||
|
deletedAt: Value(deletedAt),
|
||||||
|
channelGroupId: Value(channelGroupId),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,31 +28,75 @@ part 'get_profile_request.g.dart';
|
|||||||
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class ProfileData {
|
class ProfileData {
|
||||||
@JsonKey(name: 'user_id')
|
final int uid;
|
||||||
final String userId;
|
final String uuid;
|
||||||
|
@JsonKey(name: 'last_online')
|
||||||
|
final int lastOnline;
|
||||||
|
@JsonKey(name: 'profile_pic')
|
||||||
|
final String profilePic;
|
||||||
|
@JsonKey(name: 'profile_pic_gaussian')
|
||||||
|
final String profilePicGaussian;
|
||||||
|
final String nickname;
|
||||||
|
final String contact;
|
||||||
|
@JsonKey(name: 'country_code')
|
||||||
|
final String countryCode;
|
||||||
final String email;
|
final String email;
|
||||||
final String? nickname;
|
@JsonKey(name: 'recovery_email')
|
||||||
final String? avatar;
|
final String recoveryEmail;
|
||||||
|
final String username;
|
||||||
|
final String bio;
|
||||||
|
final int relationship;
|
||||||
|
@JsonKey(name: 'user_alias')
|
||||||
|
final String? userAlias;
|
||||||
|
@JsonKey(name: 'channel_id')
|
||||||
|
final int channelId;
|
||||||
|
@JsonKey(name: 'channel_group_id')
|
||||||
|
final int channelGroupId;
|
||||||
|
final String hint;
|
||||||
|
|
||||||
const ProfileData({
|
const ProfileData({
|
||||||
required this.userId,
|
required this.uid,
|
||||||
|
required this.uuid,
|
||||||
|
required this.lastOnline,
|
||||||
|
required this.profilePic,
|
||||||
|
required this.profilePicGaussian,
|
||||||
|
required this.nickname,
|
||||||
|
required this.contact,
|
||||||
|
required this.countryCode,
|
||||||
required this.email,
|
required this.email,
|
||||||
this.nickname,
|
required this.recoveryEmail,
|
||||||
this.avatar,
|
required this.username,
|
||||||
|
required this.bio,
|
||||||
|
required this.relationship,
|
||||||
|
this.userAlias,
|
||||||
|
required this.channelId,
|
||||||
|
required this.channelGroupId,
|
||||||
|
required this.hint,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProfileData.fromJson(Map<String, dynamic> json) =>
|
factory ProfileData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ProfileDataFromJson(json);
|
_$ProfileDataFromJson(json);
|
||||||
|
|
||||||
/// DTO → Domain Entity
|
/// DTO → Domain Entity
|
||||||
User toEntity() {
|
User toEntity() => User(
|
||||||
return User(
|
uid: uid,
|
||||||
id: userId,
|
uuid: uuid,
|
||||||
email: email,
|
lastOnline: lastOnline,
|
||||||
|
profilePic: profilePic,
|
||||||
|
profilePicGaussian: profilePicGaussian,
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
avatar: avatar,
|
contact: contact,
|
||||||
|
countryCode: countryCode,
|
||||||
|
email: email,
|
||||||
|
recoveryEmail: recoveryEmail,
|
||||||
|
username: username,
|
||||||
|
bio: bio,
|
||||||
|
relationship: relationship,
|
||||||
|
userAlias: userAlias,
|
||||||
|
channelId: channelId,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
hint: hint,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@@ -61,7 +105,7 @@ class ProfileData {
|
|||||||
|
|
||||||
/// 获取用户资料请求(GET,无参数)
|
/// 获取用户资料请求(GET,无参数)
|
||||||
///
|
///
|
||||||
/// GET 请求无 body,toJson() 返回空 map。
|
/// GET 请求无 body,mixin 自动生成 toJson() → 空 map。
|
||||||
/// 如需 query 参数(如分页),添加字段即可,
|
/// 如需 query 参数(如分页),添加字段即可,
|
||||||
/// toJson() 会自动将字段序列化为 URL query string。
|
/// toJson() 会自动将字段序列化为 URL query string。
|
||||||
@ApiRequest(
|
@ApiRequest(
|
||||||
@@ -69,14 +113,7 @@ class ProfileData {
|
|||||||
method: HttpMethod.get,
|
method: HttpMethod.get,
|
||||||
responseType: ProfileData,
|
responseType: ProfileData,
|
||||||
)
|
)
|
||||||
@JsonSerializable()
|
|
||||||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||||||
with _$GetProfileRequestApi {
|
with _$GetProfileRequestApi {
|
||||||
GetProfileRequest();
|
GetProfileRequest();
|
||||||
|
|
||||||
factory GetProfileRequest.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$GetProfileRequestFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,57 +8,140 @@ part 'login_request.g.dart';
|
|||||||
|
|
||||||
/// # /auth/login — 登录接口
|
/// # /auth/login — 登录接口
|
||||||
///
|
///
|
||||||
/// 一个端点 = 一个文件,Response DTO + Request 放在同一文件中。
|
|
||||||
///
|
|
||||||
/// ## 数据流位置
|
/// ## 数据流位置
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// AuthRepositoryImpl.login(email, password)
|
/// AuthRepositoryImpl.login(email, password)
|
||||||
/// → _client.executeRequest( ★ LoginRequest ★ ) ← 你在这里
|
/// → _client.executeRequest( ★ LoginRequest ★ ) ← 你在这里
|
||||||
/// → 服务端 POST /auth/login
|
/// → 服务端 POST /auth/login
|
||||||
/// → 响应 JSON → ★ LoginData ★ ← 也在这里
|
/// → 响应 JSON → ★ LoginResponse ★ ← 也在这里
|
||||||
/// → LoginData.toEntity() → User
|
/// → LoginResponse.toEntity() → User
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// Response DTO
|
// Response DTO
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// 登录响应 DTO
|
@JsonSerializable(createToJson: false)
|
||||||
///
|
class LoginProfile {
|
||||||
/// 服务端返回的登录数据,包含 token 和用户信息。
|
final int uid;
|
||||||
/// 通过 [toEntity] 转换为 Domain Entity [User]。
|
final String uuid;
|
||||||
@JsonSerializable()
|
@JsonKey(name: 'last_online')
|
||||||
class LoginData {
|
final int lastOnline;
|
||||||
final String token;
|
@JsonKey(name: 'profile_pic')
|
||||||
@JsonKey(name: 'user_id')
|
final String profilePic;
|
||||||
final String userId;
|
@JsonKey(name: 'profile_pic_gaussian')
|
||||||
|
final String profilePicGaussian;
|
||||||
|
final String nickname;
|
||||||
|
final String contact;
|
||||||
|
@JsonKey(name: 'country_code')
|
||||||
|
final String countryCode;
|
||||||
final String email;
|
final String email;
|
||||||
final String? nickname;
|
@JsonKey(name: 'recovery_email')
|
||||||
final String? avatar;
|
final String recoveryEmail;
|
||||||
|
final String username;
|
||||||
|
final String bio;
|
||||||
|
final int relationship;
|
||||||
|
@JsonKey(name: 'user_alias')
|
||||||
|
final String? userAlias;
|
||||||
|
@JsonKey(name: 'channel_id')
|
||||||
|
final int channelId;
|
||||||
|
@JsonKey(name: 'channel_group_id')
|
||||||
|
final int channelGroupId;
|
||||||
|
final String hint;
|
||||||
|
|
||||||
|
const LoginProfile({
|
||||||
|
required this.uid,
|
||||||
|
required this.uuid,
|
||||||
|
required this.lastOnline,
|
||||||
|
required this.profilePic,
|
||||||
|
required this.profilePicGaussian,
|
||||||
|
required this.nickname,
|
||||||
|
required this.contact,
|
||||||
|
required this.countryCode,
|
||||||
|
required this.email,
|
||||||
|
required this.recoveryEmail,
|
||||||
|
required this.username,
|
||||||
|
required this.bio,
|
||||||
|
required this.relationship,
|
||||||
|
this.userAlias,
|
||||||
|
required this.channelId,
|
||||||
|
required this.channelGroupId,
|
||||||
|
required this.hint,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginProfile.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginProfileFromJson(json);
|
||||||
|
|
||||||
|
User toEntity() => User(
|
||||||
|
uid: uid,
|
||||||
|
uuid: uuid,
|
||||||
|
lastOnline: lastOnline,
|
||||||
|
profilePic: profilePic,
|
||||||
|
profilePicGaussian: profilePicGaussian,
|
||||||
|
nickname: nickname,
|
||||||
|
contact: contact,
|
||||||
|
countryCode: countryCode,
|
||||||
|
email: email,
|
||||||
|
recoveryEmail: recoveryEmail,
|
||||||
|
username: username,
|
||||||
|
bio: bio,
|
||||||
|
relationship: relationship,
|
||||||
|
userAlias: userAlias,
|
||||||
|
channelId: channelId,
|
||||||
|
channelGroupId: channelGroupId,
|
||||||
|
hint: hint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(createToJson: false, explicitToJson: true)
|
||||||
|
class LoginData {
|
||||||
|
@JsonKey(name: 'account_id')
|
||||||
|
final String accountId;
|
||||||
|
final LoginProfile profile;
|
||||||
|
final String nonce;
|
||||||
|
@JsonKey(name: 'access_token')
|
||||||
|
final String accessToken;
|
||||||
|
@JsonKey(name: 'refresh_token')
|
||||||
|
final String refreshToken;
|
||||||
|
@JsonKey(name: 'device_id')
|
||||||
|
final String deviceId;
|
||||||
|
@JsonKey(name: 'login_data')
|
||||||
|
final String loginData;
|
||||||
|
|
||||||
const LoginData({
|
const LoginData({
|
||||||
required this.token,
|
required this.accountId,
|
||||||
required this.userId,
|
required this.profile,
|
||||||
required this.email,
|
required this.nonce,
|
||||||
this.nickname,
|
required this.accessToken,
|
||||||
this.avatar,
|
required this.refreshToken,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.loginData,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$LoginDataFromJson(json);
|
_$LoginDataFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
User toEntity() => profile.toEntity();
|
||||||
|
}
|
||||||
|
|
||||||
/// DTO → Domain Entity
|
/// Top-level envelope: { "code": 0, "message": "OK", "data": { ... } }
|
||||||
User toEntity() {
|
@JsonSerializable(createToJson: false, explicitToJson: true)
|
||||||
return User(
|
class LoginResponse {
|
||||||
id: userId,
|
final int code;
|
||||||
email: email,
|
final String message;
|
||||||
nickname: nickname,
|
final LoginData data;
|
||||||
avatar: avatar,
|
|
||||||
);
|
const LoginResponse({
|
||||||
}
|
required this.code,
|
||||||
|
required this.message,
|
||||||
|
required this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginResponseFromJson(json);
|
||||||
|
|
||||||
|
User toEntity() => data.toEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@@ -67,24 +150,21 @@ class LoginData {
|
|||||||
|
|
||||||
/// 登录请求
|
/// 登录请求
|
||||||
///
|
///
|
||||||
/// `@ApiRequest` 自动生成 `_$LoginRequestApi` mixin,
|
/// `@ApiRequest` 一个注解搞定一切:
|
||||||
/// 提供 path / method / requestType / includeToken / fromJson 自动注册。
|
/// - mixin 自动生成 path / method / requestType / includeToken / toJson
|
||||||
|
/// - toJson 只序列化类自身字段(email, password),不含继承属性
|
||||||
|
/// - Response 的 fromJson 在 parameters getter 中自动注册
|
||||||
|
/// - 无需 @JsonSerializable,无需手写 fromJson / toJson
|
||||||
@ApiRequest(
|
@ApiRequest(
|
||||||
path: ApiPaths.authLogin,
|
path: ApiPaths.authLogin,
|
||||||
method: HttpMethod.post,
|
method: HttpMethod.post,
|
||||||
responseType: LoginData,
|
responseType: LoginResponse,
|
||||||
requestType: ApiRequestType.login,
|
requestType: ApiRequestType.login,
|
||||||
)
|
)
|
||||||
@JsonSerializable()
|
class LoginRequest extends ApiRequestable<LoginResponse>
|
||||||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
with _$LoginRequestApi {
|
||||||
final String email;
|
final String email;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
LoginRequest({required this.email, required this.password});
|
LoginRequest({required this.email, required this.password});
|
||||||
|
|
||||||
factory LoginRequest.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$LoginRequestFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class UploadResult {
|
|||||||
|
|
||||||
/// FormData 上传请求
|
/// FormData 上传请求
|
||||||
///
|
///
|
||||||
/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 信封。
|
/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 格式。
|
||||||
/// 无需 override `decodeResponse`。
|
/// 无需 override `decodeResponse`。
|
||||||
@ApiRequest(
|
@ApiRequest(
|
||||||
path: ApiPaths.uploadFile,
|
path: ApiPaths.uploadFile,
|
||||||
@@ -97,7 +97,7 @@ class S3UploadResponse {
|
|||||||
/// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL)
|
/// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL)
|
||||||
/// - uploadData 为 Uint8List 二进制数据
|
/// - uploadData 为 Uint8List 二进制数据
|
||||||
/// - 自定义 headers(Content-Type: application/octet-stream)
|
/// - 自定义 headers(Content-Type: application/octet-stream)
|
||||||
/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准信封
|
/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准响应格式
|
||||||
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
|
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
|
||||||
final Uint8List data;
|
final Uint8List data;
|
||||||
final String presignedURL;
|
final String presignedURL;
|
||||||
@@ -125,7 +125,7 @@ class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
|
|||||||
@override
|
@override
|
||||||
Object? get uploadData => data;
|
Object? get uploadData => data;
|
||||||
|
|
||||||
/// S3 响应不走标准 { code, message, data } 信封,需要自定义解码
|
/// S3 响应不走标准 { code, message, data } 格式,需要自定义解码
|
||||||
///
|
///
|
||||||
/// 可能的响应:
|
/// 可能的响应:
|
||||||
/// - 204 No Content(空 body)→ 成功
|
/// - 204 No Content(空 body)→ 成功
|
||||||
|
|||||||
@@ -27,21 +27,25 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
final NetworksSdkApi _client;
|
final NetworksSdkApi _client;
|
||||||
final void Function(String?) _onTokenUpdate;
|
final void Function(String?) _onTokenUpdate;
|
||||||
|
|
||||||
AuthRepositoryImpl({required NetworksSdkApi client, required void Function(String?) onTokenUpdate,}) : _client = client, _onTokenUpdate = onTokenUpdate;
|
AuthRepositoryImpl({
|
||||||
|
required NetworksSdkApi client,
|
||||||
|
required void Function(String?) onTokenUpdate,
|
||||||
|
}) : _client = client,
|
||||||
|
_onTokenUpdate = onTokenUpdate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<User> login({required String email, required String password,}) async
|
Future<User> login({required String email, required String password}) async {
|
||||||
{
|
final LoginResponse? loginResponse = await _client.executeRequest(
|
||||||
final LoginData? loginData = await _client.executeRequest(LoginRequest(email: email, password: password),);
|
LoginRequest(email: email, password: password),
|
||||||
|
);
|
||||||
|
|
||||||
if (loginData == null) {
|
if (loginResponse == null) {
|
||||||
throw Exception('Login failed: empty response'); // TODO: 接入国际化
|
throw Exception('Login failed: empty response');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回调写入 Token(内存 + 持久化由 Provider 层组合)
|
_onTokenUpdate(loginResponse.data.accessToken);
|
||||||
_onTokenUpdate(loginData.token);
|
|
||||||
|
|
||||||
return loginData.toEntity(); // DTO → Domain Entity
|
return loginResponse.toEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
67
apps/im_app/lib/domain/entities/call_log.dart
Normal file
67
apps/im_app/lib/domain/entities/call_log.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/// 通话记录 Domain 实体
|
||||||
|
///
|
||||||
|
/// 全局共享实体,被 call / chat 等多个 Feature 共用。
|
||||||
|
/// 纯 Dart 类,零 Flutter / 零网络 / 零 DB 依赖。
|
||||||
|
class CallLog {
|
||||||
|
final String id;
|
||||||
|
final int? callerId;
|
||||||
|
final int? receiverId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? duration;
|
||||||
|
final int? videoCall;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? endedAt;
|
||||||
|
final int? status;
|
||||||
|
final int? isDeleted;
|
||||||
|
final int? deletedAt;
|
||||||
|
final int? isRead;
|
||||||
|
|
||||||
|
const CallLog({
|
||||||
|
required this.id,
|
||||||
|
this.callerId,
|
||||||
|
this.receiverId,
|
||||||
|
this.chatId,
|
||||||
|
this.duration,
|
||||||
|
this.videoCall,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.endedAt,
|
||||||
|
this.status,
|
||||||
|
this.isDeleted,
|
||||||
|
this.deletedAt,
|
||||||
|
this.isRead,
|
||||||
|
});
|
||||||
|
|
||||||
|
CallLog copyWith({
|
||||||
|
String? id,
|
||||||
|
int? callerId,
|
||||||
|
int? receiverId,
|
||||||
|
int? chatId,
|
||||||
|
int? duration,
|
||||||
|
int? videoCall,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? endedAt,
|
||||||
|
int? status,
|
||||||
|
int? isDeleted,
|
||||||
|
int? deletedAt,
|
||||||
|
int? isRead,
|
||||||
|
}) {
|
||||||
|
return CallLog(
|
||||||
|
id: id ?? this.id,
|
||||||
|
callerId: callerId ?? this.callerId,
|
||||||
|
receiverId: receiverId ?? this.receiverId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
videoCall: videoCall ?? this.videoCall,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
endedAt: endedAt ?? this.endedAt,
|
||||||
|
status: status ?? this.status,
|
||||||
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
isRead: isRead ?? this.isRead,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
200
apps/im_app/lib/domain/entities/chat.dart
Normal file
200
apps/im_app/lib/domain/entities/chat.dart
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/// 聊天 Domain 实体
|
||||||
|
class Chat {
|
||||||
|
final int id;
|
||||||
|
final int? typ;
|
||||||
|
final int? lastId;
|
||||||
|
final int? lastTyp;
|
||||||
|
final String? lastMsg;
|
||||||
|
final int? lastTime;
|
||||||
|
final int lastPos;
|
||||||
|
final int firstPos;
|
||||||
|
final int? msgIdx;
|
||||||
|
final String? profile;
|
||||||
|
final String? pin;
|
||||||
|
final String? icon;
|
||||||
|
final String iconGaussian;
|
||||||
|
final String? name;
|
||||||
|
final int? userId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? friendId;
|
||||||
|
final int? sort;
|
||||||
|
final int? unreadNum;
|
||||||
|
final int? unreadCount;
|
||||||
|
final int? hideChatMsgIdx;
|
||||||
|
final int? readChatMsgIdx;
|
||||||
|
final int? otherReadIdx;
|
||||||
|
final String? unreadAtMsgIdx;
|
||||||
|
final int? deleteTime;
|
||||||
|
final int? addIndex;
|
||||||
|
final int flag;
|
||||||
|
final int? flagMy;
|
||||||
|
final int? autoDeleteInterval;
|
||||||
|
final int? mute;
|
||||||
|
final int? verified;
|
||||||
|
final int? createTime;
|
||||||
|
final int? startIdx;
|
||||||
|
final int? isReadMsg;
|
||||||
|
final String translateOutgoing;
|
||||||
|
final String translateIncoming;
|
||||||
|
final int incomingIdx;
|
||||||
|
final int outgoingIdx;
|
||||||
|
final int incomingSoundId;
|
||||||
|
final int outgoingSoundId;
|
||||||
|
final int notificationSoundId;
|
||||||
|
final String chatKey;
|
||||||
|
final String activeChatKey;
|
||||||
|
final int coverIdx;
|
||||||
|
final int round;
|
||||||
|
final int workspaceId;
|
||||||
|
final int localPermission;
|
||||||
|
|
||||||
|
const Chat({
|
||||||
|
required this.id,
|
||||||
|
this.typ,
|
||||||
|
this.lastId,
|
||||||
|
this.lastTyp,
|
||||||
|
this.lastMsg,
|
||||||
|
this.lastTime,
|
||||||
|
this.lastPos = 0,
|
||||||
|
this.firstPos = -1,
|
||||||
|
this.msgIdx,
|
||||||
|
this.profile,
|
||||||
|
this.pin,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian = '',
|
||||||
|
this.name,
|
||||||
|
this.userId,
|
||||||
|
this.chatId,
|
||||||
|
this.friendId,
|
||||||
|
this.sort,
|
||||||
|
this.unreadNum,
|
||||||
|
this.unreadCount,
|
||||||
|
this.hideChatMsgIdx,
|
||||||
|
this.readChatMsgIdx,
|
||||||
|
this.otherReadIdx,
|
||||||
|
this.unreadAtMsgIdx,
|
||||||
|
this.deleteTime,
|
||||||
|
this.addIndex,
|
||||||
|
this.flag = 0,
|
||||||
|
this.flagMy,
|
||||||
|
this.autoDeleteInterval,
|
||||||
|
this.mute,
|
||||||
|
this.verified,
|
||||||
|
this.createTime,
|
||||||
|
this.startIdx,
|
||||||
|
this.isReadMsg,
|
||||||
|
this.translateOutgoing = '',
|
||||||
|
this.translateIncoming = '',
|
||||||
|
this.incomingIdx = 0,
|
||||||
|
this.outgoingIdx = 0,
|
||||||
|
this.incomingSoundId = 0,
|
||||||
|
this.outgoingSoundId = 0,
|
||||||
|
this.notificationSoundId = 0,
|
||||||
|
this.chatKey = '',
|
||||||
|
this.activeChatKey = '',
|
||||||
|
this.coverIdx = 0,
|
||||||
|
this.round = 0,
|
||||||
|
this.workspaceId = 0,
|
||||||
|
this.localPermission = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
Chat copyWith({
|
||||||
|
int? id,
|
||||||
|
int? typ,
|
||||||
|
int? lastId,
|
||||||
|
int? lastTyp,
|
||||||
|
String? lastMsg,
|
||||||
|
int? lastTime,
|
||||||
|
int? lastPos,
|
||||||
|
int? firstPos,
|
||||||
|
int? msgIdx,
|
||||||
|
String? profile,
|
||||||
|
String? pin,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? name,
|
||||||
|
int? userId,
|
||||||
|
int? chatId,
|
||||||
|
int? friendId,
|
||||||
|
int? sort,
|
||||||
|
int? unreadNum,
|
||||||
|
int? unreadCount,
|
||||||
|
int? hideChatMsgIdx,
|
||||||
|
int? readChatMsgIdx,
|
||||||
|
int? otherReadIdx,
|
||||||
|
String? unreadAtMsgIdx,
|
||||||
|
int? deleteTime,
|
||||||
|
int? addIndex,
|
||||||
|
int? flag,
|
||||||
|
int? flagMy,
|
||||||
|
int? autoDeleteInterval,
|
||||||
|
int? mute,
|
||||||
|
int? verified,
|
||||||
|
int? createTime,
|
||||||
|
int? startIdx,
|
||||||
|
int? isReadMsg,
|
||||||
|
String? translateOutgoing,
|
||||||
|
String? translateIncoming,
|
||||||
|
int? incomingIdx,
|
||||||
|
int? outgoingIdx,
|
||||||
|
int? incomingSoundId,
|
||||||
|
int? outgoingSoundId,
|
||||||
|
int? notificationSoundId,
|
||||||
|
String? chatKey,
|
||||||
|
String? activeChatKey,
|
||||||
|
int? coverIdx,
|
||||||
|
int? round,
|
||||||
|
int? workspaceId,
|
||||||
|
int? localPermission,
|
||||||
|
}) {
|
||||||
|
return Chat(
|
||||||
|
id: id ?? this.id,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
lastId: lastId ?? this.lastId,
|
||||||
|
lastTyp: lastTyp ?? this.lastTyp,
|
||||||
|
lastMsg: lastMsg ?? this.lastMsg,
|
||||||
|
lastTime: lastTime ?? this.lastTime,
|
||||||
|
lastPos: lastPos ?? this.lastPos,
|
||||||
|
firstPos: firstPos ?? this.firstPos,
|
||||||
|
msgIdx: msgIdx ?? this.msgIdx,
|
||||||
|
profile: profile ?? this.profile,
|
||||||
|
pin: pin ?? this.pin,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
name: name ?? this.name,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
friendId: friendId ?? this.friendId,
|
||||||
|
sort: sort ?? this.sort,
|
||||||
|
unreadNum: unreadNum ?? this.unreadNum,
|
||||||
|
unreadCount: unreadCount ?? this.unreadCount,
|
||||||
|
hideChatMsgIdx: hideChatMsgIdx ?? this.hideChatMsgIdx,
|
||||||
|
readChatMsgIdx: readChatMsgIdx ?? this.readChatMsgIdx,
|
||||||
|
otherReadIdx: otherReadIdx ?? this.otherReadIdx,
|
||||||
|
unreadAtMsgIdx: unreadAtMsgIdx ?? this.unreadAtMsgIdx,
|
||||||
|
deleteTime: deleteTime ?? this.deleteTime,
|
||||||
|
addIndex: addIndex ?? this.addIndex,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
flagMy: flagMy ?? this.flagMy,
|
||||||
|
autoDeleteInterval: autoDeleteInterval ?? this.autoDeleteInterval,
|
||||||
|
mute: mute ?? this.mute,
|
||||||
|
verified: verified ?? this.verified,
|
||||||
|
createTime: createTime ?? this.createTime,
|
||||||
|
startIdx: startIdx ?? this.startIdx,
|
||||||
|
isReadMsg: isReadMsg ?? this.isReadMsg,
|
||||||
|
translateOutgoing: translateOutgoing ?? this.translateOutgoing,
|
||||||
|
translateIncoming: translateIncoming ?? this.translateIncoming,
|
||||||
|
incomingIdx: incomingIdx ?? this.incomingIdx,
|
||||||
|
outgoingIdx: outgoingIdx ?? this.outgoingIdx,
|
||||||
|
incomingSoundId: incomingSoundId ?? this.incomingSoundId,
|
||||||
|
outgoingSoundId: outgoingSoundId ?? this.outgoingSoundId,
|
||||||
|
notificationSoundId: notificationSoundId ?? this.notificationSoundId,
|
||||||
|
chatKey: chatKey ?? this.chatKey,
|
||||||
|
activeChatKey: activeChatKey ?? this.activeChatKey,
|
||||||
|
coverIdx: coverIdx ?? this.coverIdx,
|
||||||
|
round: round ?? this.round,
|
||||||
|
workspaceId: workspaceId ?? this.workspaceId,
|
||||||
|
localPermission: localPermission ?? this.localPermission,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
apps/im_app/lib/domain/entities/chat_bot.dart
Normal file
100
apps/im_app/lib/domain/entities/chat_bot.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/// 聊天机器人 Domain 实体
|
||||||
|
class ChatBot {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final String? username;
|
||||||
|
final int? botUserId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? description;
|
||||||
|
final String? token;
|
||||||
|
final int? flag;
|
||||||
|
final int? status;
|
||||||
|
final String? webhook;
|
||||||
|
final String? commands;
|
||||||
|
final String? banner;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final int? deletedAt;
|
||||||
|
final String? internalWebhook;
|
||||||
|
final int? mode;
|
||||||
|
final String? redirectUrl;
|
||||||
|
final int? isInvitable;
|
||||||
|
final int? isAllowForward;
|
||||||
|
final String? tips;
|
||||||
|
|
||||||
|
const ChatBot({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.username,
|
||||||
|
this.botUserId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.description,
|
||||||
|
this.token,
|
||||||
|
this.flag,
|
||||||
|
this.status,
|
||||||
|
this.webhook,
|
||||||
|
this.commands,
|
||||||
|
this.banner,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.deletedAt,
|
||||||
|
this.internalWebhook,
|
||||||
|
this.mode,
|
||||||
|
this.redirectUrl,
|
||||||
|
this.isInvitable,
|
||||||
|
this.isAllowForward,
|
||||||
|
this.tips,
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatBot copyWith({
|
||||||
|
int? id,
|
||||||
|
String? name,
|
||||||
|
String? username,
|
||||||
|
int? botUserId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? description,
|
||||||
|
String? token,
|
||||||
|
int? flag,
|
||||||
|
int? status,
|
||||||
|
String? webhook,
|
||||||
|
String? commands,
|
||||||
|
String? banner,
|
||||||
|
int? channelId,
|
||||||
|
int? channelGroupId,
|
||||||
|
int? deletedAt,
|
||||||
|
String? internalWebhook,
|
||||||
|
int? mode,
|
||||||
|
String? redirectUrl,
|
||||||
|
int? isInvitable,
|
||||||
|
int? isAllowForward,
|
||||||
|
String? tips,
|
||||||
|
}) {
|
||||||
|
return ChatBot(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
username: username ?? this.username,
|
||||||
|
botUserId: botUserId ?? this.botUserId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
description: description ?? this.description,
|
||||||
|
token: token ?? this.token,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
status: status ?? this.status,
|
||||||
|
webhook: webhook ?? this.webhook,
|
||||||
|
commands: commands ?? this.commands,
|
||||||
|
banner: banner ?? this.banner,
|
||||||
|
channelId: channelId ?? this.channelId,
|
||||||
|
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
internalWebhook: internalWebhook ?? this.internalWebhook,
|
||||||
|
mode: mode ?? this.mode,
|
||||||
|
redirectUrl: redirectUrl ?? this.redirectUrl,
|
||||||
|
isInvitable: isInvitable ?? this.isInvitable,
|
||||||
|
isAllowForward: isAllowForward ?? this.isAllowForward,
|
||||||
|
tips: tips ?? this.tips,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/im_app/lib/domain/entities/chat_category.dart
Normal file
48
apps/im_app/lib/domain/entities/chat_category.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/// 聊天分类 Domain 实体
|
||||||
|
class ChatCategory {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final String? includedChatIds;
|
||||||
|
final String? excludedChatIds;
|
||||||
|
final int? seq;
|
||||||
|
final int isHide;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
|
||||||
|
const ChatCategory({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.includedChatIds,
|
||||||
|
this.excludedChatIds,
|
||||||
|
this.seq,
|
||||||
|
this.isHide = 0,
|
||||||
|
this.createdAt = 0,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatCategory copyWith({
|
||||||
|
int? id,
|
||||||
|
String? name,
|
||||||
|
String? includedChatIds,
|
||||||
|
String? excludedChatIds,
|
||||||
|
int? seq,
|
||||||
|
int? isHide,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
}) {
|
||||||
|
return ChatCategory(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
includedChatIds: includedChatIds ?? this.includedChatIds,
|
||||||
|
excludedChatIds: excludedChatIds ?? this.excludedChatIds,
|
||||||
|
seq: seq ?? this.seq,
|
||||||
|
isHide: isHide ?? this.isHide,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/im_app/lib/domain/entities/discover_mini_app.dart
Normal file
112
apps/im_app/lib/domain/entities/discover_mini_app.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// 发现小程序 Domain 实体
|
||||||
|
class DiscoverMiniApp {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final String? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const DiscoverMiniApp({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
DiscoverMiniApp copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? openuid,
|
||||||
|
String? devId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? downloadUrl,
|
||||||
|
String? description,
|
||||||
|
int? version,
|
||||||
|
int? typ,
|
||||||
|
int? flag,
|
||||||
|
int? reviewStatus,
|
||||||
|
int? favoriteAt,
|
||||||
|
int? isActive,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
double? score,
|
||||||
|
String? channels,
|
||||||
|
String? devName,
|
||||||
|
String? pictureGaussian,
|
||||||
|
String? picture,
|
||||||
|
int? commentNum,
|
||||||
|
String? lastLoginAt,
|
||||||
|
String? screen,
|
||||||
|
}) {
|
||||||
|
return DiscoverMiniApp(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
openuid: openuid ?? this.openuid,
|
||||||
|
devId: devId ?? this.devId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
reviewStatus: reviewStatus ?? this.reviewStatus,
|
||||||
|
favoriteAt: favoriteAt ?? this.favoriteAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
score: score ?? this.score,
|
||||||
|
channels: channels ?? this.channels,
|
||||||
|
devName: devName ?? this.devName,
|
||||||
|
pictureGaussian: pictureGaussian ?? this.pictureGaussian,
|
||||||
|
picture: picture ?? this.picture,
|
||||||
|
commentNum: commentNum ?? this.commentNum,
|
||||||
|
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||||
|
screen: screen ?? this.screen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/im_app/lib/domain/entities/explore_mini_app.dart
Normal file
112
apps/im_app/lib/domain/entities/explore_mini_app.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// 探索小程序 Domain 实体
|
||||||
|
class ExploreMiniApp {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const ExploreMiniApp({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
ExploreMiniApp copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? openuid,
|
||||||
|
String? devId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? downloadUrl,
|
||||||
|
String? description,
|
||||||
|
int? version,
|
||||||
|
int? typ,
|
||||||
|
int? flag,
|
||||||
|
int? reviewStatus,
|
||||||
|
int? favoriteAt,
|
||||||
|
int? isActive,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
double? score,
|
||||||
|
String? channels,
|
||||||
|
String? devName,
|
||||||
|
String? pictureGaussian,
|
||||||
|
String? picture,
|
||||||
|
int? commentNum,
|
||||||
|
int? lastLoginAt,
|
||||||
|
String? screen,
|
||||||
|
}) {
|
||||||
|
return ExploreMiniApp(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
openuid: openuid ?? this.openuid,
|
||||||
|
devId: devId ?? this.devId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
reviewStatus: reviewStatus ?? this.reviewStatus,
|
||||||
|
favoriteAt: favoriteAt ?? this.favoriteAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
score: score ?? this.score,
|
||||||
|
channels: channels ?? this.channels,
|
||||||
|
devName: devName ?? this.devName,
|
||||||
|
pictureGaussian: pictureGaussian ?? this.pictureGaussian,
|
||||||
|
picture: picture ?? this.picture,
|
||||||
|
commentNum: commentNum ?? this.commentNum,
|
||||||
|
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||||
|
screen: screen ?? this.screen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/im_app/lib/domain/entities/favorite_mini_app.dart
Normal file
112
apps/im_app/lib/domain/entities/favorite_mini_app.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// 收藏小程序 Domain 实体
|
||||||
|
class FavoriteMiniApp {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const FavoriteMiniApp({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
FavoriteMiniApp copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? openuid,
|
||||||
|
String? devId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? downloadUrl,
|
||||||
|
String? description,
|
||||||
|
int? version,
|
||||||
|
int? typ,
|
||||||
|
int? flag,
|
||||||
|
int? reviewStatus,
|
||||||
|
int? favoriteAt,
|
||||||
|
int? isActive,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
double? score,
|
||||||
|
String? channels,
|
||||||
|
String? devName,
|
||||||
|
String? pictureGaussian,
|
||||||
|
String? picture,
|
||||||
|
int? commentNum,
|
||||||
|
int? lastLoginAt,
|
||||||
|
String? screen,
|
||||||
|
}) {
|
||||||
|
return FavoriteMiniApp(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
openuid: openuid ?? this.openuid,
|
||||||
|
devId: devId ?? this.devId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
reviewStatus: reviewStatus ?? this.reviewStatus,
|
||||||
|
favoriteAt: favoriteAt ?? this.favoriteAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
score: score ?? this.score,
|
||||||
|
channels: channels ?? this.channels,
|
||||||
|
devName: devName ?? this.devName,
|
||||||
|
pictureGaussian: pictureGaussian ?? this.pictureGaussian,
|
||||||
|
picture: picture ?? this.picture,
|
||||||
|
commentNum: commentNum ?? this.commentNum,
|
||||||
|
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||||
|
screen: screen ?? this.screen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
apps/im_app/lib/domain/entities/favourite.dart
Normal file
72
apps/im_app/lib/domain/entities/favourite.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/// 收藏 Domain 实体
|
||||||
|
class Favourite {
|
||||||
|
final int id;
|
||||||
|
final String parentId;
|
||||||
|
final String data;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
final int? source;
|
||||||
|
final int? userId;
|
||||||
|
final int? authorId;
|
||||||
|
final String typ;
|
||||||
|
final String tag;
|
||||||
|
final int isPin;
|
||||||
|
final int chatTyp;
|
||||||
|
final int isUploaded;
|
||||||
|
final String urls;
|
||||||
|
|
||||||
|
const Favourite({
|
||||||
|
required this.id,
|
||||||
|
this.parentId = '',
|
||||||
|
this.data = '',
|
||||||
|
this.createdAt = 0,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
this.source,
|
||||||
|
this.userId,
|
||||||
|
this.authorId,
|
||||||
|
this.typ = '[]',
|
||||||
|
this.tag = '[]',
|
||||||
|
this.isPin = 0,
|
||||||
|
this.chatTyp = 0,
|
||||||
|
this.isUploaded = 1,
|
||||||
|
this.urls = '[]',
|
||||||
|
});
|
||||||
|
|
||||||
|
Favourite copyWith({
|
||||||
|
int? id,
|
||||||
|
String? parentId,
|
||||||
|
String? data,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
int? source,
|
||||||
|
int? userId,
|
||||||
|
int? authorId,
|
||||||
|
String? typ,
|
||||||
|
String? tag,
|
||||||
|
int? isPin,
|
||||||
|
int? chatTyp,
|
||||||
|
int? isUploaded,
|
||||||
|
String? urls,
|
||||||
|
}) {
|
||||||
|
return Favourite(
|
||||||
|
id: id ?? this.id,
|
||||||
|
parentId: parentId ?? this.parentId,
|
||||||
|
data: data ?? this.data,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
source: source ?? this.source,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
authorId: authorId ?? this.authorId,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
tag: tag ?? this.tag,
|
||||||
|
isPin: isPin ?? this.isPin,
|
||||||
|
chatTyp: chatTyp ?? this.chatTyp,
|
||||||
|
isUploaded: isUploaded ?? this.isUploaded,
|
||||||
|
urls: urls ?? this.urls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
apps/im_app/lib/domain/entities/favourite_detail.dart
Normal file
44
apps/im_app/lib/domain/entities/favourite_detail.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/// 收藏详情 Domain 实体
|
||||||
|
class FavouriteDetail {
|
||||||
|
final int? id;
|
||||||
|
final String relatedId;
|
||||||
|
final String content;
|
||||||
|
final int? typ;
|
||||||
|
final int? messageId;
|
||||||
|
final int? sendId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? sendTime;
|
||||||
|
|
||||||
|
const FavouriteDetail({
|
||||||
|
this.id,
|
||||||
|
this.relatedId = '',
|
||||||
|
this.content = '',
|
||||||
|
this.typ,
|
||||||
|
this.messageId,
|
||||||
|
this.sendId,
|
||||||
|
this.chatId,
|
||||||
|
this.sendTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
FavouriteDetail copyWith({
|
||||||
|
int? id,
|
||||||
|
String? relatedId,
|
||||||
|
String? content,
|
||||||
|
int? typ,
|
||||||
|
int? messageId,
|
||||||
|
int? sendId,
|
||||||
|
int? chatId,
|
||||||
|
int? sendTime,
|
||||||
|
}) {
|
||||||
|
return FavouriteDetail(
|
||||||
|
id: id ?? this.id,
|
||||||
|
relatedId: relatedId ?? this.relatedId,
|
||||||
|
content: content ?? this.content,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
messageId: messageId ?? this.messageId,
|
||||||
|
sendId: sendId ?? this.sendId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
sendTime: sendTime ?? this.sendTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
apps/im_app/lib/domain/entities/group.dart
Normal file
124
apps/im_app/lib/domain/entities/group.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/// 群组 Domain 实体
|
||||||
|
class Group {
|
||||||
|
final int id;
|
||||||
|
final int? userJoinDate;
|
||||||
|
final String? name;
|
||||||
|
final String? profile;
|
||||||
|
final String? icon;
|
||||||
|
final String iconGaussian;
|
||||||
|
final int? permission;
|
||||||
|
final int? admin;
|
||||||
|
final String? members;
|
||||||
|
final int? owner;
|
||||||
|
final String? admins;
|
||||||
|
final int? visible;
|
||||||
|
final int? speakInterval;
|
||||||
|
final int? groupType;
|
||||||
|
final int? roomType;
|
||||||
|
final int? maxNumber;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final int? createTime;
|
||||||
|
final int? updateTime;
|
||||||
|
final int? addIndex;
|
||||||
|
final int? maxMember;
|
||||||
|
final int? expireTime;
|
||||||
|
final int workspaceId;
|
||||||
|
final int mode;
|
||||||
|
final int redpacketPlay;
|
||||||
|
final String? topic;
|
||||||
|
final String? rp;
|
||||||
|
|
||||||
|
const Group({
|
||||||
|
required this.id,
|
||||||
|
this.userJoinDate,
|
||||||
|
this.name,
|
||||||
|
this.profile,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian = '',
|
||||||
|
this.permission,
|
||||||
|
this.admin,
|
||||||
|
this.members,
|
||||||
|
this.owner,
|
||||||
|
this.admins,
|
||||||
|
this.visible,
|
||||||
|
this.speakInterval,
|
||||||
|
this.groupType,
|
||||||
|
this.roomType,
|
||||||
|
this.maxNumber,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.createTime,
|
||||||
|
this.updateTime,
|
||||||
|
this.addIndex,
|
||||||
|
this.maxMember,
|
||||||
|
this.expireTime,
|
||||||
|
this.workspaceId = 0,
|
||||||
|
this.mode = 0,
|
||||||
|
this.redpacketPlay = 0,
|
||||||
|
this.topic,
|
||||||
|
this.rp,
|
||||||
|
});
|
||||||
|
|
||||||
|
Group copyWith({
|
||||||
|
int? id,
|
||||||
|
int? userJoinDate,
|
||||||
|
String? name,
|
||||||
|
String? profile,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
int? permission,
|
||||||
|
int? admin,
|
||||||
|
String? members,
|
||||||
|
int? owner,
|
||||||
|
String? admins,
|
||||||
|
int? visible,
|
||||||
|
int? speakInterval,
|
||||||
|
int? groupType,
|
||||||
|
int? roomType,
|
||||||
|
int? maxNumber,
|
||||||
|
int? channelId,
|
||||||
|
int? channelGroupId,
|
||||||
|
int? createTime,
|
||||||
|
int? updateTime,
|
||||||
|
int? addIndex,
|
||||||
|
int? maxMember,
|
||||||
|
int? expireTime,
|
||||||
|
int? workspaceId,
|
||||||
|
int? mode,
|
||||||
|
int? redpacketPlay,
|
||||||
|
String? topic,
|
||||||
|
String? rp,
|
||||||
|
}) {
|
||||||
|
return Group(
|
||||||
|
id: id ?? this.id,
|
||||||
|
userJoinDate: userJoinDate ?? this.userJoinDate,
|
||||||
|
name: name ?? this.name,
|
||||||
|
profile: profile ?? this.profile,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
permission: permission ?? this.permission,
|
||||||
|
admin: admin ?? this.admin,
|
||||||
|
members: members ?? this.members,
|
||||||
|
owner: owner ?? this.owner,
|
||||||
|
admins: admins ?? this.admins,
|
||||||
|
visible: visible ?? this.visible,
|
||||||
|
speakInterval: speakInterval ?? this.speakInterval,
|
||||||
|
groupType: groupType ?? this.groupType,
|
||||||
|
roomType: roomType ?? this.roomType,
|
||||||
|
maxNumber: maxNumber ?? this.maxNumber,
|
||||||
|
channelId: channelId ?? this.channelId,
|
||||||
|
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||||
|
createTime: createTime ?? this.createTime,
|
||||||
|
updateTime: updateTime ?? this.updateTime,
|
||||||
|
addIndex: addIndex ?? this.addIndex,
|
||||||
|
maxMember: maxMember ?? this.maxMember,
|
||||||
|
expireTime: expireTime ?? this.expireTime,
|
||||||
|
workspaceId: workspaceId ?? this.workspaceId,
|
||||||
|
mode: mode ?? this.mode,
|
||||||
|
redpacketPlay: redpacketPlay ?? this.redpacketPlay,
|
||||||
|
topic: topic ?? this.topic,
|
||||||
|
rp: rp ?? this.rp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
apps/im_app/lib/domain/entities/message.dart
Normal file
76
apps/im_app/lib/domain/entities/message.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/// 消息 Domain 实体
|
||||||
|
class Message {
|
||||||
|
final int id;
|
||||||
|
final int? messageId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? chatIdx;
|
||||||
|
final int? sendId;
|
||||||
|
final String? content;
|
||||||
|
final int? typ;
|
||||||
|
final int? sendTime;
|
||||||
|
final int? expireTime;
|
||||||
|
final int? createTime;
|
||||||
|
final String? atUsers;
|
||||||
|
final String emojis;
|
||||||
|
final int editTime;
|
||||||
|
final int refTyp;
|
||||||
|
final int flag;
|
||||||
|
final String cmid;
|
||||||
|
|
||||||
|
const Message({
|
||||||
|
required this.id,
|
||||||
|
this.messageId,
|
||||||
|
this.chatId,
|
||||||
|
this.chatIdx,
|
||||||
|
this.sendId,
|
||||||
|
this.content,
|
||||||
|
this.typ,
|
||||||
|
this.sendTime,
|
||||||
|
this.expireTime,
|
||||||
|
this.createTime,
|
||||||
|
this.atUsers,
|
||||||
|
this.emojis = '[]',
|
||||||
|
this.editTime = 0,
|
||||||
|
this.refTyp = 0,
|
||||||
|
this.flag = 0,
|
||||||
|
this.cmid = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
Message copyWith({
|
||||||
|
int? id,
|
||||||
|
int? messageId,
|
||||||
|
int? chatId,
|
||||||
|
int? chatIdx,
|
||||||
|
int? sendId,
|
||||||
|
String? content,
|
||||||
|
int? typ,
|
||||||
|
int? sendTime,
|
||||||
|
int? expireTime,
|
||||||
|
int? createTime,
|
||||||
|
String? atUsers,
|
||||||
|
String? emojis,
|
||||||
|
int? editTime,
|
||||||
|
int? refTyp,
|
||||||
|
int? flag,
|
||||||
|
String? cmid,
|
||||||
|
}) {
|
||||||
|
return Message(
|
||||||
|
id: id ?? this.id,
|
||||||
|
messageId: messageId ?? this.messageId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
chatIdx: chatIdx ?? this.chatIdx,
|
||||||
|
sendId: sendId ?? this.sendId,
|
||||||
|
content: content ?? this.content,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
sendTime: sendTime ?? this.sendTime,
|
||||||
|
expireTime: expireTime ?? this.expireTime,
|
||||||
|
createTime: createTime ?? this.createTime,
|
||||||
|
atUsers: atUsers ?? this.atUsers,
|
||||||
|
emojis: emojis ?? this.emojis,
|
||||||
|
editTime: editTime ?? this.editTime,
|
||||||
|
refTyp: refTyp ?? this.refTyp,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
cmid: cmid ?? this.cmid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/// 待处理好友请求历史 Domain 实体
|
||||||
|
class PendingFriendRequestHistory {
|
||||||
|
final int id;
|
||||||
|
final int uid;
|
||||||
|
final int requestTime;
|
||||||
|
final String? remarks;
|
||||||
|
final String? source;
|
||||||
|
final int? rs;
|
||||||
|
|
||||||
|
const PendingFriendRequestHistory({
|
||||||
|
required this.id,
|
||||||
|
required this.uid,
|
||||||
|
required this.requestTime,
|
||||||
|
this.remarks,
|
||||||
|
this.source,
|
||||||
|
this.rs,
|
||||||
|
});
|
||||||
|
|
||||||
|
PendingFriendRequestHistory copyWith({
|
||||||
|
int? id,
|
||||||
|
int? uid,
|
||||||
|
int? requestTime,
|
||||||
|
String? remarks,
|
||||||
|
String? source,
|
||||||
|
int? rs,
|
||||||
|
}) {
|
||||||
|
return PendingFriendRequestHistory(
|
||||||
|
id: id ?? this.id,
|
||||||
|
uid: uid ?? this.uid,
|
||||||
|
requestTime: requestTime ?? this.requestTime,
|
||||||
|
remarks: remarks ?? this.remarks,
|
||||||
|
source: source ?? this.source,
|
||||||
|
rs: rs ?? this.rs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/im_app/lib/domain/entities/recent_mini_app.dart
Normal file
112
apps/im_app/lib/domain/entities/recent_mini_app.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// 最近小程序 Domain 实体
|
||||||
|
class RecentMiniApp {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final String? openuid;
|
||||||
|
final String? devId;
|
||||||
|
final String? icon;
|
||||||
|
final String? iconGaussian;
|
||||||
|
final String? downloadUrl;
|
||||||
|
final String? description;
|
||||||
|
final int? version;
|
||||||
|
final int? typ;
|
||||||
|
final int? flag;
|
||||||
|
final int? reviewStatus;
|
||||||
|
final int? favoriteAt;
|
||||||
|
final int? isActive;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final double? score;
|
||||||
|
final String? channels;
|
||||||
|
final String? devName;
|
||||||
|
final String? pictureGaussian;
|
||||||
|
final String? picture;
|
||||||
|
final int? commentNum;
|
||||||
|
final int? lastLoginAt;
|
||||||
|
final String? screen;
|
||||||
|
|
||||||
|
const RecentMiniApp({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.openuid,
|
||||||
|
this.devId,
|
||||||
|
this.icon,
|
||||||
|
this.iconGaussian,
|
||||||
|
this.downloadUrl,
|
||||||
|
this.description,
|
||||||
|
this.version,
|
||||||
|
this.typ,
|
||||||
|
this.flag,
|
||||||
|
this.reviewStatus,
|
||||||
|
this.favoriteAt,
|
||||||
|
this.isActive,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.score,
|
||||||
|
this.channels,
|
||||||
|
this.devName,
|
||||||
|
this.pictureGaussian,
|
||||||
|
this.picture,
|
||||||
|
this.commentNum,
|
||||||
|
this.lastLoginAt,
|
||||||
|
this.screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
RecentMiniApp copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? openuid,
|
||||||
|
String? devId,
|
||||||
|
String? icon,
|
||||||
|
String? iconGaussian,
|
||||||
|
String? downloadUrl,
|
||||||
|
String? description,
|
||||||
|
int? version,
|
||||||
|
int? typ,
|
||||||
|
int? flag,
|
||||||
|
int? reviewStatus,
|
||||||
|
int? favoriteAt,
|
||||||
|
int? isActive,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
double? score,
|
||||||
|
String? channels,
|
||||||
|
String? devName,
|
||||||
|
String? pictureGaussian,
|
||||||
|
String? picture,
|
||||||
|
int? commentNum,
|
||||||
|
int? lastLoginAt,
|
||||||
|
String? screen,
|
||||||
|
}) {
|
||||||
|
return RecentMiniApp(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
openuid: openuid ?? this.openuid,
|
||||||
|
devId: devId ?? this.devId,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
iconGaussian: iconGaussian ?? this.iconGaussian,
|
||||||
|
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
flag: flag ?? this.flag,
|
||||||
|
reviewStatus: reviewStatus ?? this.reviewStatus,
|
||||||
|
favoriteAt: favoriteAt ?? this.favoriteAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
score: score ?? this.score,
|
||||||
|
channels: channels ?? this.channels,
|
||||||
|
devName: devName ?? this.devName,
|
||||||
|
pictureGaussian: pictureGaussian ?? this.pictureGaussian,
|
||||||
|
picture: picture ?? this.picture,
|
||||||
|
commentNum: commentNum ?? this.commentNum,
|
||||||
|
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||||
|
screen: screen ?? this.screen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
apps/im_app/lib/domain/entities/retry.dart
Normal file
60
apps/im_app/lib/domain/entities/retry.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/// 重试 Domain 实体
|
||||||
|
class Retry {
|
||||||
|
final int? id;
|
||||||
|
final int? uid;
|
||||||
|
final String apiType;
|
||||||
|
final String endPoint;
|
||||||
|
final String requestData;
|
||||||
|
final int? synced;
|
||||||
|
final String callbackFun;
|
||||||
|
final int? expired;
|
||||||
|
final int? replace;
|
||||||
|
final int? expireTime;
|
||||||
|
final int? createTime;
|
||||||
|
final int? addIndex;
|
||||||
|
|
||||||
|
const Retry({
|
||||||
|
this.id,
|
||||||
|
this.uid,
|
||||||
|
this.apiType = '',
|
||||||
|
this.endPoint = '',
|
||||||
|
this.requestData = '',
|
||||||
|
this.synced,
|
||||||
|
this.callbackFun = '',
|
||||||
|
this.expired,
|
||||||
|
this.replace,
|
||||||
|
this.expireTime,
|
||||||
|
this.createTime,
|
||||||
|
this.addIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
Retry copyWith({
|
||||||
|
int? id,
|
||||||
|
int? uid,
|
||||||
|
String? apiType,
|
||||||
|
String? endPoint,
|
||||||
|
String? requestData,
|
||||||
|
int? synced,
|
||||||
|
String? callbackFun,
|
||||||
|
int? expired,
|
||||||
|
int? replace,
|
||||||
|
int? expireTime,
|
||||||
|
int? createTime,
|
||||||
|
int? addIndex,
|
||||||
|
}) {
|
||||||
|
return Retry(
|
||||||
|
id: id ?? this.id,
|
||||||
|
uid: uid ?? this.uid,
|
||||||
|
apiType: apiType ?? this.apiType,
|
||||||
|
endPoint: endPoint ?? this.endPoint,
|
||||||
|
requestData: requestData ?? this.requestData,
|
||||||
|
synced: synced ?? this.synced,
|
||||||
|
callbackFun: callbackFun ?? this.callbackFun,
|
||||||
|
expired: expired ?? this.expired,
|
||||||
|
replace: replace ?? this.replace,
|
||||||
|
expireTime: expireTime ?? this.expireTime,
|
||||||
|
createTime: createTime ?? this.createTime,
|
||||||
|
addIndex: addIndex ?? this.addIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/im_app/lib/domain/entities/sound.dart
Normal file
48
apps/im_app/lib/domain/entities/sound.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/// 音效 Domain 实体
|
||||||
|
class Sound {
|
||||||
|
final int id;
|
||||||
|
final String filePath;
|
||||||
|
final int typ;
|
||||||
|
final String name;
|
||||||
|
final int createdAt;
|
||||||
|
final int updatedAt;
|
||||||
|
final int deletedAt;
|
||||||
|
final int channelGroupId;
|
||||||
|
final int isDefault;
|
||||||
|
|
||||||
|
const Sound({
|
||||||
|
required this.id,
|
||||||
|
this.filePath = '',
|
||||||
|
required this.typ,
|
||||||
|
this.name = '',
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.deletedAt = 0,
|
||||||
|
required this.channelGroupId,
|
||||||
|
required this.isDefault,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sound copyWith({
|
||||||
|
int? id,
|
||||||
|
String? filePath,
|
||||||
|
int? typ,
|
||||||
|
String? name,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
int? channelGroupId,
|
||||||
|
int? isDefault,
|
||||||
|
}) {
|
||||||
|
return Sound(
|
||||||
|
id: id ?? this.id,
|
||||||
|
filePath: filePath ?? this.filePath,
|
||||||
|
typ: typ ?? this.typ,
|
||||||
|
name: name ?? this.name,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||||
|
isDefault: isDefault ?? this.isDefault,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
apps/im_app/lib/domain/entities/tag.dart
Normal file
40
apps/im_app/lib/domain/entities/tag.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/// 标签 Domain 实体
|
||||||
|
class Tag {
|
||||||
|
final int? id;
|
||||||
|
final int? uid;
|
||||||
|
final String name;
|
||||||
|
final int? type;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? addIndex;
|
||||||
|
|
||||||
|
const Tag({
|
||||||
|
this.id,
|
||||||
|
this.uid,
|
||||||
|
this.name = '',
|
||||||
|
this.type,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.addIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag copyWith({
|
||||||
|
int? id,
|
||||||
|
int? uid,
|
||||||
|
String? name,
|
||||||
|
int? type,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? addIndex,
|
||||||
|
}) {
|
||||||
|
return Tag(
|
||||||
|
id: id ?? this.id,
|
||||||
|
uid: uid ?? this.uid,
|
||||||
|
name: name ?? this.name,
|
||||||
|
type: type ?? this.type,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
addIndex: addIndex ?? this.addIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,15 +14,81 @@
|
|||||||
/// → View 渲染
|
/// → View 渲染
|
||||||
/// ```
|
/// ```
|
||||||
class User {
|
class User {
|
||||||
final String id;
|
final int uid;
|
||||||
final String email;
|
final String? uuid;
|
||||||
|
final int? lastOnline;
|
||||||
|
final String? profilePic;
|
||||||
|
final String? profilePicGaussian;
|
||||||
final String? nickname;
|
final String? nickname;
|
||||||
final String? avatar;
|
final String? contact;
|
||||||
|
final String? countryCode;
|
||||||
|
final String? email;
|
||||||
|
final String? recoveryEmail;
|
||||||
|
final String? username;
|
||||||
|
final String? bio;
|
||||||
|
final int? relationship;
|
||||||
|
final String? userAlias;
|
||||||
|
final int? channelId;
|
||||||
|
final int? channelGroupId;
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
const User({
|
const User({
|
||||||
required this.id,
|
required this.uid,
|
||||||
required this.email,
|
this.uuid,
|
||||||
|
this.lastOnline,
|
||||||
|
this.profilePic,
|
||||||
|
this.profilePicGaussian,
|
||||||
this.nickname,
|
this.nickname,
|
||||||
this.avatar,
|
this.contact,
|
||||||
|
this.countryCode,
|
||||||
|
this.email,
|
||||||
|
this.recoveryEmail,
|
||||||
|
this.username,
|
||||||
|
this.bio,
|
||||||
|
this.relationship,
|
||||||
|
this.userAlias,
|
||||||
|
this.channelId,
|
||||||
|
this.channelGroupId,
|
||||||
|
this.hint,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
User copyWith({
|
||||||
|
int? uid,
|
||||||
|
String? uuid,
|
||||||
|
int? lastOnline,
|
||||||
|
String? profilePic,
|
||||||
|
String? profilePicGaussian,
|
||||||
|
String? nickname,
|
||||||
|
String? contact,
|
||||||
|
String? countryCode,
|
||||||
|
String? email,
|
||||||
|
String? recoveryEmail,
|
||||||
|
String? username,
|
||||||
|
String? bio,
|
||||||
|
int? relationship,
|
||||||
|
String? userAlias,
|
||||||
|
int? channelId,
|
||||||
|
int? channelGroupId,
|
||||||
|
String? hint,
|
||||||
|
}) {
|
||||||
|
return User(
|
||||||
|
uid: uid ?? this.uid,
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
lastOnline: lastOnline ?? this.lastOnline,
|
||||||
|
profilePic: profilePic ?? this.profilePic,
|
||||||
|
profilePicGaussian: profilePicGaussian ?? this.profilePicGaussian,
|
||||||
|
nickname: nickname ?? this.nickname,
|
||||||
|
contact: contact ?? this.contact,
|
||||||
|
countryCode: countryCode ?? this.countryCode,
|
||||||
|
email: email ?? this.email,
|
||||||
|
recoveryEmail: recoveryEmail ?? this.recoveryEmail,
|
||||||
|
username: username ?? this.username,
|
||||||
|
bio: bio ?? this.bio,
|
||||||
|
relationship: relationship ?? this.relationship,
|
||||||
|
userAlias: userAlias ?? this.userAlias,
|
||||||
|
channelId: channelId ?? this.channelId,
|
||||||
|
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||||
|
hint: hint ?? this.hint,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
apps/im_app/lib/domain/entities/user_request_history.dart
Normal file
16
apps/im_app/lib/domain/entities/user_request_history.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// 用户请求历史 Domain 实体
|
||||||
|
class UserRequestHistory {
|
||||||
|
final int id;
|
||||||
|
final int? status;
|
||||||
|
final int? createdAt;
|
||||||
|
|
||||||
|
const UserRequestHistory({required this.id, this.status, this.createdAt});
|
||||||
|
|
||||||
|
UserRequestHistory copyWith({int? id, int? status, int? createdAt}) {
|
||||||
|
return UserRequestHistory(
|
||||||
|
id: id ?? this.id,
|
||||||
|
status: status ?? this.status,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/im_app/lib/domain/entities/workspace.dart
Normal file
64
apps/im_app/lib/domain/entities/workspace.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/// 工作空间 Domain 实体
|
||||||
|
class Workspace {
|
||||||
|
final int id;
|
||||||
|
final String? name;
|
||||||
|
final int? ownerId;
|
||||||
|
final String? description;
|
||||||
|
final String? logo;
|
||||||
|
final int? grade;
|
||||||
|
final int? cap;
|
||||||
|
final String? currency;
|
||||||
|
final int? status;
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int? deletedAt;
|
||||||
|
final int? channelGroupId;
|
||||||
|
|
||||||
|
const Workspace({
|
||||||
|
required this.id,
|
||||||
|
this.name,
|
||||||
|
this.ownerId,
|
||||||
|
this.description,
|
||||||
|
this.logo,
|
||||||
|
this.grade,
|
||||||
|
this.cap,
|
||||||
|
this.currency,
|
||||||
|
this.status,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
this.channelGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Workspace copyWith({
|
||||||
|
int? id,
|
||||||
|
String? name,
|
||||||
|
int? ownerId,
|
||||||
|
String? description,
|
||||||
|
String? logo,
|
||||||
|
int? grade,
|
||||||
|
int? cap,
|
||||||
|
String? currency,
|
||||||
|
int? status,
|
||||||
|
int? createdAt,
|
||||||
|
int? updatedAt,
|
||||||
|
int? deletedAt,
|
||||||
|
int? channelGroupId,
|
||||||
|
}) {
|
||||||
|
return Workspace(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
description: description ?? this.description,
|
||||||
|
logo: logo ?? this.logo,
|
||||||
|
grade: grade ?? this.grade,
|
||||||
|
cap: cap ?? this.cap,
|
||||||
|
currency: currency ?? this.currency,
|
||||||
|
status: status ?? this.status,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
channelGroupId: channelGroupId ?? this.channelGroupId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,17 +58,16 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
|||||||
return ChatDbTestState(testResults: testResults);
|
return ChatDbTestState(testResults: testResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
|
// ── 操作(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
|
||||||
|
|
||||||
/// 开始测试
|
/// 切换测试状态(开始 / 停止)
|
||||||
void startDBTest(BuildContext context) {
|
void toggleDBTest() {
|
||||||
|
if (state.testStarted) {
|
||||||
|
state = state.copyWith(testStarted: false, currentState: '结束测试');
|
||||||
|
} else {
|
||||||
state = state.copyWith(testStarted: true, currentState: '开始测试');
|
state = state.copyWith(testStarted: true, currentState: '开始测试');
|
||||||
_testDBInsert();
|
_testDBInsert();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 结束测试
|
|
||||||
void stopDBTest(BuildContext context) {
|
|
||||||
state = state.copyWith(testStarted: false, currentState: '结束测试');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _testDBInsert() async {
|
Future<void> _testDBInsert() async {
|
||||||
@@ -84,13 +83,11 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
|||||||
for (var i = 0; i < count; i += chunkSize) {
|
for (var i = 0; i < count; i += chunkSize) {
|
||||||
final chunk = List.generate(
|
final chunk = List.generate(
|
||||||
chunkSize.clamp(0, count - i),
|
chunkSize.clamp(0, count - i),
|
||||||
(j) => UsersCompanion.insert(
|
(j) =>
|
||||||
uid: Value(i + j),
|
UsersCompanion.insert(uid: i + j, nickname: Value('User ${i + j}')),
|
||||||
nickname: Value('User ${i + j}'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.batchInsertOrReplace<User>(chunk);
|
await db.batchInsertOrReplace<DriftUser>(chunk);
|
||||||
completed += chunk.length;
|
completed += chunk.length;
|
||||||
|
|
||||||
// 让出主线程
|
// 让出主线程
|
||||||
|
|||||||
@@ -3,19 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:im_app/features/chat/presentation/chat_db_test_view_model.dart';
|
import 'package:im_app/features/chat/presentation/chat_db_test_view_model.dart';
|
||||||
|
|
||||||
import '../../../core/ui/components/app_button.dart';
|
import '../../../core/ui/components/app_button.dart';
|
||||||
import '../presentation/chat_view_model.dart';
|
|
||||||
|
|
||||||
/// 聊天页(Demo 按钮)
|
/// 数据库性能测试页(Demo)
|
||||||
///
|
///
|
||||||
/// 包含五个演示按钮,覆盖 go_router 的常见导航场景:
|
/// 批量插入 10000 条用户记录,验证 Drift 批量写入性能。
|
||||||
/// - 「切换 Tab」 — go,替换历史,不可返回
|
/// 所有操作通过 [ChatDbTestViewModel] 处理,View 只负责渲染。
|
||||||
/// - 「有参 push(extra)」 — push + extra(Dart Record),可返回
|
/// 正式开发后此页面将被删除。
|
||||||
/// - 「有参 push(路径参数)」— push + URL 内嵌 id,可返回
|
|
||||||
/// - 「无参 push」 — push,可返回
|
|
||||||
/// - 「退出登录」 — 守卫自动重定向到 /login
|
|
||||||
///
|
|
||||||
/// 所有操作通过 [ChatViewModel] 处理,View 不直接调用路由。
|
|
||||||
/// 正式开发后替换为会话列表,按钮相关代码一并清除。
|
|
||||||
class ChatDbTestPage extends ConsumerWidget {
|
class ChatDbTestPage extends ConsumerWidget {
|
||||||
const ChatDbTestPage({super.key});
|
const ChatDbTestPage({super.key});
|
||||||
|
|
||||||
@@ -25,7 +18,7 @@ class ChatDbTestPage extends ConsumerWidget {
|
|||||||
final state = ref.watch(chatDbTestViewModelProvider);
|
final state = ref.watch(chatDbTestViewModelProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('测试数据库'), ),
|
appBar: AppBar(title: const Text('测试数据库')),
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
@@ -38,17 +31,14 @@ class ChatDbTestPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
AppButton.inverse(
|
AppButton.inverse(
|
||||||
label: state.testStarted ? '结束' : '开始',
|
label: state.testStarted ? '结束' : '开始',
|
||||||
onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context),
|
onPressed: () => vm.toggleDBTest(),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(state.currentState, textAlign: TextAlign.end),
|
||||||
state.currentState,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -63,7 +53,7 @@ class ChatDbTestPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:im_app/data/models/user_dto.dart';
|
||||||
|
import 'package:im_app/data/remote/login_request.dart';
|
||||||
import 'package:networks_sdk/networks_sdk.dart';
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
import 'package:im_app/app/di/db_provider.dart';
|
import 'package:im_app/app/di/db_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -55,16 +59,25 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
/// 仅用于演示路由守卫行为,正式 UI 就绪后删除此方法,改用 [login]。
|
/// 仅用于演示路由守卫行为,正式 UI 就绪后删除此方法,改用 [login]。
|
||||||
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
|
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
|
||||||
Future<void> demoLogin() async {
|
Future<void> demoLogin() async {
|
||||||
|
try {
|
||||||
final storageApi = ref.read(storageSdkProvider);
|
final storageApi = ref.read(storageSdkProvider);
|
||||||
|
|
||||||
///TODO: StorageSDKLifeCycle 需要只在主项目暴露
|
|
||||||
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
||||||
ref.read(authNotifierProvider).login();
|
|
||||||
|
|
||||||
await storageLifeCycle.openDatabase(1234567);
|
// 读取 mock 数据
|
||||||
final rows = await storageApi.rawQuery("PRAGMA table_info('user')");
|
final raw = await rootBundle.loadString('assets/loginData.json');
|
||||||
for (final row in rows) {
|
final json = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
debugPrint('Schema: ${row.data}');
|
final loginResponse = LoginResponse.fromJson(json);
|
||||||
|
final user = loginResponse.data.toEntity();
|
||||||
|
|
||||||
|
// 先完成 DB 操作,再标记登录状态(失败时不会误标为已登录)
|
||||||
|
await storageLifeCycle.openDatabase(user.uid);
|
||||||
|
final userCompanion = UserDto.fromEntity(user).toCompanion();
|
||||||
|
await storageApi.insert(userCompanion);
|
||||||
|
|
||||||
|
// 全部成功后再更新登录状态,触发路由守卫重定向
|
||||||
|
ref.read(authNotifierProvider).login();
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(error: e.toString(), isLoading: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,3 +66,5 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/presentation/facade/cipher_guard_sdk_api.dart';
|
export 'src/presentation/facade/cipher_guard_sdk_api.dart';
|
||||||
|
export 'src/data/datasources/encryption_flutter_service.dart' show KdfMode;
|
||||||
export 'src/domain/entities/rsa_key_pair.dart';
|
export 'src/domain/entities/rsa_key_pair.dart';
|
||||||
export 'src/domain/entities/session_key.dart';
|
export 'src/domain/entities/session_key.dart';
|
||||||
export 'src/domain/entities/encrypted_message.dart';
|
export 'src/domain/entities/encrypted_message.dart';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:isolate';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@@ -7,30 +8,96 @@ import 'package:crypto/crypto.dart';
|
|||||||
import 'package:encrypt/encrypt.dart' as encrypt_pkg;
|
import 'package:encrypt/encrypt.dart' as encrypt_pkg;
|
||||||
import 'package:pointycastle/api.dart';
|
import 'package:pointycastle/api.dart';
|
||||||
import 'package:pointycastle/asymmetric/api.dart';
|
import 'package:pointycastle/asymmetric/api.dart';
|
||||||
|
import 'package:pointycastle/asymmetric/pkcs1.dart';
|
||||||
|
import 'package:pointycastle/asymmetric/rsa.dart';
|
||||||
|
import 'package:pointycastle/digests/sha256.dart';
|
||||||
|
import 'package:pointycastle/key_derivators/api.dart';
|
||||||
|
import 'package:pointycastle/key_derivators/pbkdf2.dart';
|
||||||
import 'package:pointycastle/key_generators/api.dart';
|
import 'package:pointycastle/key_generators/api.dart';
|
||||||
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
|
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
|
||||||
|
import 'package:pointycastle/macs/hmac.dart';
|
||||||
import 'package:pointycastle/random/fortuna_random.dart';
|
import 'package:pointycastle/random/fortuna_random.dart';
|
||||||
import 'package:pointycastle/asymmetric/rsa.dart';
|
|
||||||
import 'package:pointycastle/asymmetric/pkcs1.dart';
|
|
||||||
|
|
||||||
/// Flutter Encryption Service
|
/// 密钥派生模式
|
||||||
/// Implements all encryption logic in Flutter using pointycastle and encrypt packages
|
///
|
||||||
/// Replaces native Android/iOS encryption implementations
|
/// 决定 [EncryptionFlutterService._deriveKeyForRound] 使用哪种算法。
|
||||||
|
/// 默认 [md5](UU 兼容),可选 [pbkdf2](增强安全性)。
|
||||||
|
///
|
||||||
|
/// 解密旧数据时必须使用加密时相同的模式,
|
||||||
|
/// 通过消息的 version 字段区分。
|
||||||
|
enum KdfMode {
|
||||||
|
/// MD5 简单哈希(UU 兼容默认模式)
|
||||||
|
///
|
||||||
|
/// 适用于 session key 已是 32 字节强随机值的场景。
|
||||||
|
/// 性能好,每次调用 < 0.1ms。
|
||||||
|
md5,
|
||||||
|
|
||||||
|
/// PBKDF2-HMAC-SHA256(可选增强模式)
|
||||||
|
///
|
||||||
|
/// 适用于从弱密码派生密钥的场景。
|
||||||
|
/// 性能取决于迭代次数,10000 次约 10-50ms。
|
||||||
|
pbkdf2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flutter 加密服务
|
||||||
|
///
|
||||||
|
/// 端对端加密的核心引擎,纯 Dart 实现。
|
||||||
|
/// 使用 pointycastle(RSA)+ encrypt(AES)+ crypto(MD5)。
|
||||||
|
///
|
||||||
|
/// ## 性能优化
|
||||||
|
///
|
||||||
|
/// - **RSA 密钥生成**:通过 [generateRsaKeyPairAsync] 在 Isolate 中运行,
|
||||||
|
/// 避免阻塞主线程(1024-bit 约 150ms,2048-bit 约 300ms)
|
||||||
|
/// - **派生密钥缓存**:[_deriveKeyForRound] 结果按 (sessionKey, round) 缓存,
|
||||||
|
/// 同一 session 的重复加解密直接命中缓存
|
||||||
|
/// - **Random.secure() 复用**:全局单例,不再每次调用创建新实例
|
||||||
|
/// - **KDF 双模式**:MD5(默认,UU 兼容)/ PBKDF2(可选,增强安全性)
|
||||||
class EncryptionFlutterService {
|
class EncryptionFlutterService {
|
||||||
// ==================== Constants ====================
|
// ==================== 配置 ====================
|
||||||
|
|
||||||
|
/// 密钥派生模式,默认 MD5(UU 兼容)
|
||||||
|
final KdfMode kdfMode;
|
||||||
|
|
||||||
|
/// PBKDF2 迭代次数(仅 PBKDF2 模式有效,默认 10000)
|
||||||
|
final int pbkdf2Iterations;
|
||||||
|
|
||||||
|
EncryptionFlutterService({
|
||||||
|
this.kdfMode = KdfMode.md5,
|
||||||
|
this.pbkdf2Iterations = 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 常量 ====================
|
||||||
|
|
||||||
static const int sessionKeySize = 32;
|
static const int sessionKeySize = 32;
|
||||||
static const int gcmIvLength = 12;
|
static const int gcmIvLength = 12;
|
||||||
|
static const int _maxDerivedKeyCacheSize = 64;
|
||||||
|
|
||||||
// ==================== RSA Key Management ====================
|
// ==================== 性能优化:复用 Random 实例 ====================
|
||||||
|
|
||||||
/// Generate RSA key pair in PEM format
|
/// 全局 Random.secure() 单例,避免每次调用创建新实例
|
||||||
|
static final Random _secureRandom = Random.secure();
|
||||||
|
|
||||||
|
// ==================== 性能优化:派生密钥 LRU 缓存 ====================
|
||||||
|
|
||||||
|
/// 派生密钥缓存:'sessionKey:round:mode' -> Uint8List
|
||||||
|
///
|
||||||
|
/// 同一 session + round 的加解密只派生一次,后续直接命中缓存。
|
||||||
|
/// LinkedHashMap 保持插入顺序,满时淘汰最早条目。
|
||||||
|
final _derivedKeyCache = <String, Uint8List>{};
|
||||||
|
|
||||||
|
/// 清空派生密钥缓存(session key 轮换时调用)
|
||||||
|
void clearDerivedKeyCache() => _derivedKeyCache.clear();
|
||||||
|
|
||||||
|
// ==================== RSA 密钥管理 ====================
|
||||||
|
|
||||||
|
/// 生成 RSA 密钥对(同步,阻塞主线程)
|
||||||
|
///
|
||||||
|
/// 建议使用 [generateRsaKeyPairAsync] 代替,避免 UI 卡顿。
|
||||||
RsaKeyPairResult generateRsaKeyPair({int keySize = 1024}) {
|
RsaKeyPairResult generateRsaKeyPair({int keySize = 1024}) {
|
||||||
try {
|
try {
|
||||||
// Get secure random
|
|
||||||
final secureRandom = FortunaRandom();
|
final secureRandom = FortunaRandom();
|
||||||
secureRandom.seed(KeyParameter(_generateSecureRandomBytes(32)));
|
secureRandom.seed(KeyParameter(_generateSecureRandomBytes(32)));
|
||||||
|
|
||||||
// Create RSA key generator
|
|
||||||
final keyGen = RSAKeyGenerator();
|
final keyGen = RSAKeyGenerator();
|
||||||
keyGen.init(
|
keyGen.init(
|
||||||
ParametersWithRandom(
|
ParametersWithRandom(
|
||||||
@@ -39,12 +106,10 @@ class EncryptionFlutterService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate key pair
|
|
||||||
final keyPair = keyGen.generateKeyPair();
|
final keyPair = keyGen.generateKeyPair();
|
||||||
final rsaPublicKey = keyPair.publicKey;
|
final rsaPublicKey = keyPair.publicKey;
|
||||||
final rsaPrivateKey = keyPair.privateKey;
|
final rsaPrivateKey = keyPair.privateKey;
|
||||||
|
|
||||||
// Export to PEM format
|
|
||||||
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
||||||
final privateKeyPem = _encodeRSAPrivateKey(rsaPrivateKey);
|
final privateKeyPem = _encodeRSAPrivateKey(rsaPrivateKey);
|
||||||
|
|
||||||
@@ -57,26 +122,38 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode RSA public key to PEM format using asn1lib
|
/// 生成 RSA 密钥对(异步,在 Isolate 中运行,不阻塞主线程)
|
||||||
|
///
|
||||||
|
/// RSA 密钥生成是 CPU 密集型操作(1024-bit 约 150ms,2048-bit 约 300ms),
|
||||||
|
/// 放在 Isolate 中避免主线程卡顿。
|
||||||
|
///
|
||||||
|
/// **Isolate 隔离说明**:
|
||||||
|
/// Isolate 内会创建一个**默认配置**的 EncryptionFlutterService(KdfMode.md5),
|
||||||
|
/// 不会继承当前实例的 kdfMode / pbkdf2Iterations。
|
||||||
|
/// 这对 RSA 密钥生成没有影响(RSA 不走 KDF),但如果将来需要在
|
||||||
|
/// Isolate 中执行依赖 KDF 的操作(如消息加解密),需要传递配置参数。
|
||||||
|
Future<RsaKeyPairResult> generateRsaKeyPairAsync({int keySize = 1024}) async {
|
||||||
|
return await Isolate.run(
|
||||||
|
() => EncryptionFlutterService().generateRsaKeyPair(keySize: keySize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 编码 RSA 公钥为 PEM 格式
|
||||||
String _encodeRSAPublicKey(RSAPublicKey publicKey) {
|
String _encodeRSAPublicKey(RSAPublicKey publicKey) {
|
||||||
// Build RSAPublicKeyInfo structure
|
|
||||||
final topSeq = ASN1Sequence();
|
final topSeq = ASN1Sequence();
|
||||||
|
|
||||||
// AlgorithmIdentifier: OID 1.2.840.113549.1.1.1 + NULL
|
|
||||||
final algoSeq = ASN1Sequence();
|
final algoSeq = ASN1Sequence();
|
||||||
algoSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1])); // RSA
|
algoSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1]));
|
||||||
algoSeq.add(ASN1Null());
|
algoSeq.add(ASN1Null());
|
||||||
topSeq.add(algoSeq);
|
topSeq.add(algoSeq);
|
||||||
|
|
||||||
// RSAPublicKey: modulus + publicExponent
|
|
||||||
final keySeq = ASN1Sequence();
|
final keySeq = ASN1Sequence();
|
||||||
keySeq.add(ASN1Integer(publicKey.n!));
|
keySeq.add(ASN1Integer(publicKey.n!));
|
||||||
keySeq.add(ASN1Integer(publicKey.exponent!));
|
keySeq.add(ASN1Integer(publicKey.exponent!));
|
||||||
|
|
||||||
// BitString wrapping the key (with 0 unused bits prefix)
|
|
||||||
final keyBytes = keySeq.encodedBytes;
|
final keyBytes = keySeq.encodedBytes;
|
||||||
final keyList = List<int>.from(keyBytes);
|
final keyList = List<int>.from(keyBytes);
|
||||||
keyList.insert(0, 0); // Add unused bits byte
|
keyList.insert(0, 0);
|
||||||
topSeq.add(ASN1BitString(keyList));
|
topSeq.add(ASN1BitString(keyList));
|
||||||
|
|
||||||
final derBytes = topSeq.encodedBytes;
|
final derBytes = topSeq.encodedBytes;
|
||||||
@@ -84,51 +161,32 @@ class EncryptionFlutterService {
|
|||||||
return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----';
|
return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode RSA private key to PEM format using asn1lib
|
/// 编码 RSA 私钥为 PEM 格式
|
||||||
String _encodeRSAPrivateKey(RSAPrivateKey privateKey) {
|
String _encodeRSAPrivateKey(RSAPrivateKey privateKey) {
|
||||||
// Build RSAPrivateKey structure (PKCS#8 format)
|
|
||||||
final topSeq = ASN1Sequence();
|
final topSeq = ASN1Sequence();
|
||||||
|
|
||||||
// Version (0)
|
|
||||||
topSeq.add(ASN1Integer(BigInt.zero));
|
topSeq.add(ASN1Integer(BigInt.zero));
|
||||||
|
|
||||||
// Modulus
|
|
||||||
topSeq.add(ASN1Integer(privateKey.n!));
|
topSeq.add(ASN1Integer(privateKey.n!));
|
||||||
|
|
||||||
// Public Exponent
|
|
||||||
topSeq.add(ASN1Integer(privateKey.exponent!));
|
topSeq.add(ASN1Integer(privateKey.exponent!));
|
||||||
|
|
||||||
// Private Exponent
|
|
||||||
topSeq.add(ASN1Integer(privateKey.privateExponent!));
|
topSeq.add(ASN1Integer(privateKey.privateExponent!));
|
||||||
|
|
||||||
// Prime P
|
|
||||||
topSeq.add(ASN1Integer(privateKey.p!));
|
topSeq.add(ASN1Integer(privateKey.p!));
|
||||||
|
|
||||||
// Prime Q
|
|
||||||
topSeq.add(ASN1Integer(privateKey.q!));
|
topSeq.add(ASN1Integer(privateKey.q!));
|
||||||
|
|
||||||
// (Optional CRT params omitted for simplicity)
|
|
||||||
|
|
||||||
final derBytes = topSeq.encodedBytes;
|
final derBytes = topSeq.encodedBytes;
|
||||||
final base64 = base64Encode(derBytes.toList());
|
final base64 = base64Encode(derBytes.toList());
|
||||||
return '-----BEGIN PRIVATE KEY-----\n$base64\n-----END PRIVATE KEY-----';
|
return '-----BEGIN PRIVATE KEY-----\n$base64\n-----END PRIVATE KEY-----';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Private Key Encryption/Decryption ====================
|
// ==================== 私钥加密/解密 ====================
|
||||||
|
|
||||||
/// Encrypt private key with password (AES-CBC with MD5-derived key)
|
/// 用密码加密私钥(AES-CBC,密码通过 MD5 派生密钥)
|
||||||
String encryptPrivateKey({
|
String encryptPrivateKey({
|
||||||
required String privateKey,
|
required String privateKey,
|
||||||
required String password,
|
required String password,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Generate AES key from MD5(password)
|
|
||||||
final aesKey = _md5Hash(password);
|
final aesKey = _md5Hash(password);
|
||||||
|
|
||||||
// Generate random IV (16 bytes)
|
|
||||||
final iv = _generateSecureRandomBytes(16);
|
final iv = _generateSecureRandomBytes(16);
|
||||||
|
|
||||||
// AES encrypt using encrypt package
|
|
||||||
final secretKey = encrypt_pkg.Key(aesKey);
|
final secretKey = encrypt_pkg.Key(aesKey);
|
||||||
final encryptor = encrypt_pkg.Encrypter(
|
final encryptor = encrypt_pkg.Encrypter(
|
||||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||||
@@ -137,7 +195,6 @@ class EncryptionFlutterService {
|
|||||||
final encrypted = encryptor.encrypt(privateKey, iv: encrypt_pkg.IV(iv));
|
final encrypted = encryptor.encrypt(privateKey, iv: encrypt_pkg.IV(iv));
|
||||||
final encryptedBytes = encrypted.bytes;
|
final encryptedBytes = encrypted.bytes;
|
||||||
|
|
||||||
// Combine IV + encrypted data
|
|
||||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||||
combined.setAll(0, iv);
|
combined.setAll(0, iv);
|
||||||
combined.setAll(iv.length, encryptedBytes);
|
combined.setAll(iv.length, encryptedBytes);
|
||||||
@@ -148,23 +205,17 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt private key with password (AES-CBC with MD5-derived key)
|
/// 用密码解密私钥(AES-CBC,密码通过 MD5 派生密钥)
|
||||||
String decryptPrivateKey({
|
String decryptPrivateKey({
|
||||||
required String encryptedPrivateKey,
|
required String encryptedPrivateKey,
|
||||||
required String password,
|
required String password,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Generate AES key from MD5(password)
|
|
||||||
final aesKey = _md5Hash(password);
|
final aesKey = _md5Hash(password);
|
||||||
|
|
||||||
// Decode Base64
|
|
||||||
final combined = base64Decode(encryptedPrivateKey);
|
final combined = base64Decode(encryptedPrivateKey);
|
||||||
|
|
||||||
// Extract IV and encrypted data
|
|
||||||
final iv = combined.sublist(0, 16);
|
final iv = combined.sublist(0, 16);
|
||||||
final encBytes = combined.sublist(16);
|
final encBytes = combined.sublist(16);
|
||||||
|
|
||||||
// AES decrypt
|
|
||||||
final secretKey = encrypt_pkg.Key(aesKey);
|
final secretKey = encrypt_pkg.Key(aesKey);
|
||||||
final encryptor = encrypt_pkg.Encrypter(
|
final encryptor = encrypt_pkg.Encrypter(
|
||||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||||
@@ -181,9 +232,9 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Session Key Management ====================
|
// ==================== 会话密钥管理 ====================
|
||||||
|
|
||||||
/// Generate session key (32 bytes random)
|
/// 生成会话密钥(32 字节随机)
|
||||||
SessionKeyResult generateSessionKey({int initialRound = 1}) {
|
SessionKeyResult generateSessionKey({int initialRound = 1}) {
|
||||||
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
||||||
final key = base64Encode(keyBytes);
|
final key = base64Encode(keyBytes);
|
||||||
@@ -191,16 +242,14 @@ class EncryptionFlutterService {
|
|||||||
return SessionKeyResult(key: key, round: initialRound);
|
return SessionKeyResult(key: key, round: initialRound);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt session key with RSA public key
|
/// 用 RSA 公钥加密会话密钥
|
||||||
String encryptSessionKey({
|
String encryptSessionKey({
|
||||||
required String sessionKey,
|
required String sessionKey,
|
||||||
required String publicKey,
|
required String publicKey,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Parse RSA public key
|
|
||||||
final rsaPublicKey = _parsePublicKey(publicKey);
|
final rsaPublicKey = _parsePublicKey(publicKey);
|
||||||
|
|
||||||
// RSA encrypt using PKCS1 padding (like native implementations)
|
|
||||||
final cipher = PKCS1Encoding(RSAEngine());
|
final cipher = PKCS1Encoding(RSAEngine());
|
||||||
cipher.init(true, PublicKeyParameter<RSAPublicKey>(rsaPublicKey));
|
cipher.init(true, PublicKeyParameter<RSAPublicKey>(rsaPublicKey));
|
||||||
|
|
||||||
@@ -211,16 +260,14 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt session key with RSA private key
|
/// 用 RSA 私钥解密会话密钥
|
||||||
String decryptSessionKey({
|
String decryptSessionKey({
|
||||||
required String encryptedSessionKey,
|
required String encryptedSessionKey,
|
||||||
required String privateKey,
|
required String privateKey,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Parse RSA private key
|
|
||||||
final rsaPrivateKey = _parsePrivateKey(privateKey);
|
final rsaPrivateKey = _parsePrivateKey(privateKey);
|
||||||
|
|
||||||
// RSA decrypt using PKCS1 padding (like native implementations)
|
|
||||||
final cipher = PKCS1Encoding(RSAEngine());
|
final cipher = PKCS1Encoding(RSAEngine());
|
||||||
cipher.init(false, PrivateKeyParameter<RSAPrivateKey>(rsaPrivateKey));
|
cipher.init(false, PrivateKeyParameter<RSAPrivateKey>(rsaPrivateKey));
|
||||||
|
|
||||||
@@ -231,22 +278,18 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Message Encryption/Decryption ====================
|
// ==================== 消息加密/解密 ====================
|
||||||
|
|
||||||
/// Encrypt message (AES-CTR with round-based key derivation)
|
/// 加密消息(AES-CTR,使用 round 派生密钥)
|
||||||
EncryptedMessageResult encryptMessage({
|
EncryptedMessageResult encryptMessage({
|
||||||
required String plaintext,
|
required String plaintext,
|
||||||
required String sessionKey,
|
required String sessionKey,
|
||||||
required int round,
|
required int round,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Derive key for round
|
|
||||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||||
|
|
||||||
// Generate random IV (16 bytes for CTR)
|
|
||||||
final iv = _generateSecureRandomBytes(16);
|
final iv = _generateSecureRandomBytes(16);
|
||||||
|
|
||||||
// AES-CTR encrypt
|
|
||||||
final secretKey = encrypt_pkg.Key(actualKey);
|
final secretKey = encrypt_pkg.Key(actualKey);
|
||||||
final encryptor = encrypt_pkg.Encrypter(
|
final encryptor = encrypt_pkg.Encrypter(
|
||||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||||
@@ -255,7 +298,6 @@ class EncryptionFlutterService {
|
|||||||
final encrypted = encryptor.encrypt(plaintext, iv: encrypt_pkg.IV(iv));
|
final encrypted = encryptor.encrypt(plaintext, iv: encrypt_pkg.IV(iv));
|
||||||
final encryptedBytes = encrypted.bytes;
|
final encryptedBytes = encrypted.bytes;
|
||||||
|
|
||||||
// Combine IV + encrypted data
|
|
||||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||||
combined.setAll(0, iv);
|
combined.setAll(0, iv);
|
||||||
combined.setAll(iv.length, encryptedBytes);
|
combined.setAll(iv.length, encryptedBytes);
|
||||||
@@ -268,24 +310,18 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt message (AES-CTR with round-based key derivation)
|
/// 解密消息(AES-CTR,使用 round 派生密钥)
|
||||||
String decryptMessage({
|
String decryptMessage({
|
||||||
required String encryptedData,
|
required String encryptedData,
|
||||||
required String sessionKey,
|
required String sessionKey,
|
||||||
required int round,
|
required int round,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Derive key for round
|
|
||||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||||
|
|
||||||
// Decode Base64
|
|
||||||
final combined = base64Decode(encryptedData);
|
final combined = base64Decode(encryptedData);
|
||||||
|
|
||||||
// Extract IV and encrypted data
|
|
||||||
final iv = combined.sublist(0, 16);
|
final iv = combined.sublist(0, 16);
|
||||||
final encBytes = combined.sublist(16);
|
final encBytes = combined.sublist(16);
|
||||||
|
|
||||||
// AES-CTR decrypt
|
|
||||||
final secretKey = encrypt_pkg.Key(actualKey);
|
final secretKey = encrypt_pkg.Key(actualKey);
|
||||||
final encryptor = encrypt_pkg.Encrypter(
|
final encryptor = encrypt_pkg.Encrypter(
|
||||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||||
@@ -302,16 +338,16 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Push Notification Decryption ====================
|
// ==================== 推送通知解密 ====================
|
||||||
|
|
||||||
/// Set AES secret for push notification decryption
|
/// 设置 AES secret(用于推送通知解密)
|
||||||
void setAesSecret(String aesSecret) {
|
void setAesSecret(String aesSecret) {
|
||||||
_aesSecret = aesSecret;
|
_aesSecret = aesSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _aesSecret;
|
String? _aesSecret;
|
||||||
|
|
||||||
/// Decrypt push notification (AES-GCM)
|
/// 解密推送通知(AES-GCM)
|
||||||
String decryptPushNotification({required String encryptedData}) {
|
String decryptPushNotification({required String encryptedData}) {
|
||||||
try {
|
try {
|
||||||
final secret = _aesSecret;
|
final secret = _aesSecret;
|
||||||
@@ -319,17 +355,11 @@ class EncryptionFlutterService {
|
|||||||
throw Exception('AES_SECRET not set');
|
throw Exception('AES_SECRET not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert hex string to bytes
|
|
||||||
final secretBytes = _hexStringToBytes(secret);
|
final secretBytes = _hexStringToBytes(secret);
|
||||||
|
|
||||||
// Decode Base64
|
|
||||||
final combined = base64Decode(encryptedData);
|
final combined = base64Decode(encryptedData);
|
||||||
|
|
||||||
// Extract IV and encrypted data
|
|
||||||
final iv = combined.sublist(0, gcmIvLength);
|
final iv = combined.sublist(0, gcmIvLength);
|
||||||
final encBytes = combined.sublist(gcmIvLength);
|
final encBytes = combined.sublist(gcmIvLength);
|
||||||
|
|
||||||
// AES-GCM decrypt
|
|
||||||
final secretKey = encrypt_pkg.Key(secretBytes);
|
final secretKey = encrypt_pkg.Key(secretBytes);
|
||||||
final encryptor = encrypt_pkg.Encrypter(
|
final encryptor = encrypt_pkg.Encrypter(
|
||||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.gcm),
|
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.gcm),
|
||||||
@@ -346,37 +376,91 @@ class EncryptionFlutterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Helper Methods ====================
|
// ==================== 内部方法 ====================
|
||||||
|
|
||||||
/// Generate secure random bytes
|
/// 生成安全随机字节(复用全局 Random.secure() 实例)
|
||||||
Uint8List _generateSecureRandomBytes(int length) {
|
Uint8List _generateSecureRandomBytes(int length) {
|
||||||
final random = Random.secure();
|
|
||||||
final bytes = Uint8List(length);
|
final bytes = Uint8List(length);
|
||||||
for (var i = 0; i < length; i++) {
|
for (var i = 0; i < length; i++) {
|
||||||
bytes[i] = random.nextInt(256);
|
bytes[i] = _secureRandom.nextInt(256);
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// MD5 hash
|
/// MD5 哈希(用于密码派生密钥)
|
||||||
Uint8List _md5Hash(String input) {
|
Uint8List _md5Hash(String input) {
|
||||||
final bytes = utf8.encode(input);
|
final bytes = utf8.encode(input);
|
||||||
final hash = md5.convert(bytes).bytes as Uint8List;
|
final hash = md5.convert(bytes).bytes;
|
||||||
return hash;
|
return Uint8List.fromList(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive key for round (MD5 hash of session key)
|
/// 按 round 派生 AES 密钥(带 LRU 缓存)
|
||||||
|
///
|
||||||
|
/// 支持两种模式:
|
||||||
|
/// - [KdfMode.md5]:MD5(sessionKey + round),兼容模式,< 0.1ms
|
||||||
|
/// - [KdfMode.pbkdf2]:PBKDF2-HMAC-SHA256(sessionKey, salt=round),约 10-50ms
|
||||||
|
///
|
||||||
|
/// 两种模式都会将 round 参与派生计算,保证不同 round 产出不同密钥。
|
||||||
|
/// 缓存命中时直接返回,跳过计算。
|
||||||
|
/// 缓存满时淘汰最久未访问的条目(LRU)。
|
||||||
Uint8List _deriveKeyForRound(String sessionKey, int targetRound) {
|
Uint8List _deriveKeyForRound(String sessionKey, int targetRound) {
|
||||||
// Base64 decode session key
|
final modeName = kdfMode == KdfMode.md5 ? 'md5' : 'pbkdf2';
|
||||||
final keyBytes = base64Decode(sessionKey);
|
final cacheKey = '$sessionKey:$targetRound:$modeName';
|
||||||
|
|
||||||
// Apply MD5 for the round (simplified version)
|
// 缓存命中 — 移至末尾以维护 LRU 顺序
|
||||||
final hash = md5.convert(keyBytes).bytes as Uint8List;
|
final cached = _derivedKeyCache.remove(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
return hash;
|
_derivedKeyCache[cacheKey] = cached;
|
||||||
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse RSA public key from PEM
|
// 计算派生密钥
|
||||||
|
final Uint8List result;
|
||||||
|
switch (kdfMode) {
|
||||||
|
case KdfMode.md5:
|
||||||
|
// 将 sessionKey + round 一起参与 hash,保证不同 round 产出不同密钥
|
||||||
|
final keyBytes = base64Decode(sessionKey);
|
||||||
|
final roundBytes = utf8.encode(':$targetRound');
|
||||||
|
final combined = Uint8List(keyBytes.length + roundBytes.length)
|
||||||
|
..setRange(0, keyBytes.length, keyBytes)
|
||||||
|
..setRange(
|
||||||
|
keyBytes.length,
|
||||||
|
keyBytes.length + roundBytes.length,
|
||||||
|
roundBytes,
|
||||||
|
);
|
||||||
|
final hash = md5.convert(combined).bytes;
|
||||||
|
result = Uint8List.fromList(hash);
|
||||||
|
case KdfMode.pbkdf2:
|
||||||
|
result = _pbkdf2Derive(sessionKey, targetRound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LRU 淘汰:满时移除最久未访问的条目(Map 头部)
|
||||||
|
if (_derivedKeyCache.length >= _maxDerivedKeyCacheSize) {
|
||||||
|
_derivedKeyCache.remove(_derivedKeyCache.keys.first);
|
||||||
|
}
|
||||||
|
_derivedKeyCache[cacheKey] = result;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PBKDF2-HMAC-SHA256 密钥派生
|
||||||
|
///
|
||||||
|
/// salt 包含 round 信息,不同 round 派生不同密钥。
|
||||||
|
/// 迭代次数由 [pbkdf2Iterations] 控制(默认 10000)。
|
||||||
|
/// 输出 16 字节(AES-128 密钥)。
|
||||||
|
Uint8List _pbkdf2Derive(String sessionKey, int targetRound) {
|
||||||
|
final keyBytes = base64Decode(sessionKey);
|
||||||
|
final salt = utf8.encode('round:$targetRound');
|
||||||
|
|
||||||
|
final derivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
|
||||||
|
derivator.init(
|
||||||
|
Pbkdf2Parameters(Uint8List.fromList(salt), pbkdf2Iterations, 16),
|
||||||
|
);
|
||||||
|
|
||||||
|
return derivator.process(Uint8List.fromList(keyBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 RSA 公钥 PEM
|
||||||
RSAPublicKey _parsePublicKey(String pem) {
|
RSAPublicKey _parsePublicKey(String pem) {
|
||||||
final base64 = pem
|
final base64 = pem
|
||||||
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
|
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
|
||||||
@@ -385,7 +469,6 @@ class EncryptionFlutterService {
|
|||||||
.trim();
|
.trim();
|
||||||
final bytes = base64Decode(base64);
|
final bytes = base64Decode(base64);
|
||||||
|
|
||||||
// Parse ASN.1 DER format
|
|
||||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||||
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||||
|
|
||||||
@@ -403,7 +486,7 @@ class EncryptionFlutterService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse RSA private key from PEM
|
/// 解析 RSA 私钥 PEM
|
||||||
RSAPrivateKey _parsePrivateKey(String pem) {
|
RSAPrivateKey _parsePrivateKey(String pem) {
|
||||||
final base64 = pem
|
final base64 = pem
|
||||||
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
|
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
|
||||||
@@ -412,7 +495,6 @@ class EncryptionFlutterService {
|
|||||||
.trim();
|
.trim();
|
||||||
final bytes = base64Decode(base64);
|
final bytes = base64Decode(base64);
|
||||||
|
|
||||||
// Parse ASN.1 DER format
|
|
||||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||||
final keySeq = asn1Parser.nextObject() as ASN1Sequence;
|
final keySeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||||
|
|
||||||
@@ -429,7 +511,7 @@ class EncryptionFlutterService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert hex string to bytes
|
/// Hex 字符串转字节
|
||||||
Uint8List _hexStringToBytes(String hex) {
|
Uint8List _hexStringToBytes(String hex) {
|
||||||
final len = hex.length;
|
final len = hex.length;
|
||||||
final data = Uint8List(len ~/ 2);
|
final data = Uint8List(len ~/ 2);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024}) async {
|
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024}) async {
|
||||||
final result = _service.generateRsaKeyPair(keySize: keySize);
|
// 在 Isolate 中运行,避免阻塞主线程(1024-bit 约 150ms)
|
||||||
|
final result = await _service.generateRsaKeyPairAsync(keySize: keySize);
|
||||||
return RsaKeyPair(
|
return RsaKeyPair(
|
||||||
publicKey: result.publicKey,
|
publicKey: result.publicKey,
|
||||||
privateKey: result.privateKey,
|
privateKey: result.privateKey,
|
||||||
@@ -50,10 +51,7 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
|
|||||||
@override
|
@override
|
||||||
Future<SessionKey> generateSessionKey({int initialRound = 1}) async {
|
Future<SessionKey> generateSessionKey({int initialRound = 1}) async {
|
||||||
final result = _service.generateSessionKey(initialRound: initialRound);
|
final result = _service.generateSessionKey(initialRound: initialRound);
|
||||||
return SessionKey(
|
return SessionKey(key: result.key, round: result.round);
|
||||||
key: result.key,
|
|
||||||
round: result.round,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -91,10 +89,7 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
|
|||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
round: round,
|
round: round,
|
||||||
);
|
);
|
||||||
return EncryptedMessage(
|
return EncryptedMessage(round: result.round, data: result.data);
|
||||||
round: result.round,
|
|
||||||
data: result.data,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -110,6 +105,11 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 缓存管理 ====================
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearDerivedKeyCache() => _service.clearDerivedKeyCache();
|
||||||
|
|
||||||
// ==================== 原生平台同步 ====================
|
// ==================== 原生平台同步 ====================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -147,4 +147,3 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
|
|||||||
return _service.decryptPushNotification(encryptedData: encryptedData);
|
return _service.decryptPushNotification(encryptedData: encryptedData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ abstract class EncryptionRepository {
|
|||||||
required Map<String, Map<String, dynamic>> chatMap,
|
required Map<String, Map<String, dynamic>> chatMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== 缓存管理 ====================
|
||||||
|
|
||||||
|
/// 清空派生密钥缓存
|
||||||
|
///
|
||||||
|
/// 在 session key 轮换时调用,确保旧密钥的派生结果不会被复用。
|
||||||
|
/// 不影响已加密的消息,只影响后续加解密操作的密钥派生。
|
||||||
|
void clearDerivedKeyCache();
|
||||||
|
|
||||||
// ==================== 配置相關 ====================
|
// ==================== 配置相關 ====================
|
||||||
|
|
||||||
/// 設置 AES_SECRET (用於推送解密)
|
/// 設置 AES_SECRET (用於推送解密)
|
||||||
@@ -91,8 +99,5 @@ abstract class EncryptionRepository {
|
|||||||
|
|
||||||
/// 解密 APNS 推送通知內容
|
/// 解密 APNS 推送通知內容
|
||||||
/// 使用 release.json 中的 AES_SECRET
|
/// 使用 release.json 中的 AES_SECRET
|
||||||
Future<String?> decryptPushNotification({
|
Future<String?> decryptPushNotification({required String encryptedData});
|
||||||
required String encryptedData,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import 'package:cipher_guard_sdk/src/domain/entities/session_key.dart';
|
|||||||
import 'package:cipher_guard_sdk/src/domain/entities/encrypted_message.dart';
|
import 'package:cipher_guard_sdk/src/domain/entities/encrypted_message.dart';
|
||||||
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_wiring.dart';
|
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_wiring.dart';
|
||||||
|
|
||||||
abstract class CipherGuardSdkApi
|
abstract class CipherGuardSdkApi {
|
||||||
{
|
|
||||||
factory CipherGuardSdkApi() => CipherGuardSdkWiring.build();
|
factory CipherGuardSdkApi() => CipherGuardSdkWiring.build();
|
||||||
|
|
||||||
// ==================== 平台版本 ====================
|
// ==================== 平台版本 ====================
|
||||||
@@ -22,10 +21,16 @@ abstract class CipherGuardSdkApi
|
|||||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024});
|
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024});
|
||||||
|
|
||||||
/// 用密碼加密私鑰
|
/// 用密碼加密私鑰
|
||||||
Future<String> encryptPrivateKey({required String privateKey, required String password,});
|
Future<String> encryptPrivateKey({
|
||||||
|
required String privateKey,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
/// 解密私鑰
|
/// 解密私鑰
|
||||||
Future<String> decryptPrivateKey({required String encryptedPrivateKey, required String password,});
|
Future<String> decryptPrivateKey({
|
||||||
|
required String encryptedPrivateKey,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 會話金鑰管理 ====================
|
// ==================== 會話金鑰管理 ====================
|
||||||
|
|
||||||
@@ -33,26 +38,56 @@ abstract class CipherGuardSdkApi
|
|||||||
Future<SessionKey> generateSessionKey({int initialRound = 1});
|
Future<SessionKey> generateSessionKey({int initialRound = 1});
|
||||||
|
|
||||||
/// 用 RSA 公鑰加密會話金鑰
|
/// 用 RSA 公鑰加密會話金鑰
|
||||||
Future<String> encryptSessionKey({required String sessionKey, required String publicKey,});
|
Future<String> encryptSessionKey({
|
||||||
|
required String sessionKey,
|
||||||
|
required String publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
/// 用 RSA 私鑰解密會話金鑰
|
/// 用 RSA 私鑰解密會話金鑰
|
||||||
Future<String> decryptSessionKey({required String encryptedSessionKey, required String privateKey,});
|
Future<String> decryptSessionKey({
|
||||||
|
required String encryptedSessionKey,
|
||||||
|
required String privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 訊息加解密 ====================
|
// ==================== 訊息加解密 ====================
|
||||||
|
|
||||||
/// 加密訊息
|
/// 加密訊息
|
||||||
Future<EncryptedMessage> encryptMessage({required String plaintext, required String sessionKey, required int round,});
|
Future<EncryptedMessage> encryptMessage({
|
||||||
|
required String plaintext,
|
||||||
|
required String sessionKey,
|
||||||
|
required int round,
|
||||||
|
});
|
||||||
|
|
||||||
/// 解密訊息
|
/// 解密訊息
|
||||||
Future<String> decryptMessage({required String encryptedData, required String sessionKey, required int round,});
|
Future<String> decryptMessage({
|
||||||
|
required String encryptedData,
|
||||||
|
required String sessionKey,
|
||||||
|
required int round,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 缓存管理 ====================
|
||||||
|
|
||||||
|
/// 清空派生密钥缓存
|
||||||
|
///
|
||||||
|
/// session key 轮换后必须调用,否则旧 key 的派生结果可能被复用,
|
||||||
|
/// 导致加解密使用错误的密钥。
|
||||||
|
void clearDerivedKeyCache();
|
||||||
|
|
||||||
// ==================== 原生平台同步 ====================
|
// ==================== 原生平台同步 ====================
|
||||||
|
|
||||||
/// 同步加密金鑰到原生平台 (iOS App Group)
|
/// 同步加密金鑰到原生平台 (iOS App Group)
|
||||||
Future<void> syncEncryptionKey({required String chatId, required int activeRound, required int round, required String activeKey, required bool isSingle,});
|
Future<void> syncEncryptionKey({
|
||||||
|
required String chatId,
|
||||||
|
required int activeRound,
|
||||||
|
required int round,
|
||||||
|
required String activeKey,
|
||||||
|
required bool isSingle,
|
||||||
|
});
|
||||||
|
|
||||||
/// 批量同步所有加密聊天室的金鑰
|
/// 批量同步所有加密聊天室的金鑰
|
||||||
Future<void> syncAllEncryptionKeys({required Map<String, Map<String, dynamic>> chatMap,});
|
Future<void> syncAllEncryptionKeys({
|
||||||
|
required Map<String, Map<String, dynamic>> chatMap,
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 推送通知解密 ====================
|
// ==================== 推送通知解密 ====================
|
||||||
|
|
||||||
@@ -60,6 +95,5 @@ abstract class CipherGuardSdkApi
|
|||||||
Future<void> setAesSecret({required String aesSecret});
|
Future<void> setAesSecret({required String aesSecret});
|
||||||
|
|
||||||
/// 解密 APNS 推送通知內容
|
/// 解密 APNS 推送通知內容
|
||||||
Future<String?> decryptPushNotification({required String encryptedData,});
|
Future<String?> decryptPushNotification({required String encryptedData});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearDerivedKeyCache() => _core.encryptionRepo.clearDerivedKeyCache();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> syncEncryptionKey({
|
Future<void> syncEncryptionKey({
|
||||||
required String chatId,
|
required String chatId,
|
||||||
@@ -123,9 +126,9 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> decryptPushNotification({
|
Future<String?> decryptPushNotification({required String encryptedData}) {
|
||||||
required String encryptedData,
|
return _core.encryptionRepo.decryptPushNotification(
|
||||||
}) {
|
encryptedData: encryptedData,
|
||||||
return _core.encryptionRepo.decryptPushNotification(encryptedData: encryptedData);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,18 @@ import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_api_im
|
|||||||
/// 使用 Flutter 本地加密服務,無需原生平台處理加密邏輯
|
/// 使用 Flutter 本地加密服務,無需原生平台處理加密邏輯
|
||||||
class CipherGuardSdkWiring {
|
class CipherGuardSdkWiring {
|
||||||
/// 構建 SDK 實例
|
/// 構建 SDK 實例
|
||||||
static CipherGuardSdkApi build() {
|
///
|
||||||
|
/// [kdfMode] — 密钥派生模式,默认 [KdfMode.md5](兼容模式)
|
||||||
|
/// [pbkdf2Iterations] — PBKDF2 迭代次数(仅 pbkdf2 模式生效,默认 10000)
|
||||||
|
static CipherGuardSdkApi build({
|
||||||
|
KdfMode kdfMode = KdfMode.md5,
|
||||||
|
int pbkdf2Iterations = 10000,
|
||||||
|
}) {
|
||||||
// 1. 創建 Flutter 加密服務
|
// 1. 創建 Flutter 加密服務
|
||||||
final flutterService = EncryptionFlutterService();
|
final flutterService = EncryptionFlutterService(
|
||||||
|
kdfMode: kdfMode,
|
||||||
|
pbkdf2Iterations: pbkdf2Iterations,
|
||||||
|
);
|
||||||
|
|
||||||
// 2. 創建 Repository (使用 Flutter 服務)
|
// 2. 創建 Repository (使用 Flutter 服務)
|
||||||
final repository = EncryptionRepositoryImpl(flutterService);
|
final repository = EncryptionRepositoryImpl(flutterService);
|
||||||
@@ -39,4 +48,3 @@ class _CipherGuardPlatformImpl implements CipherGuardPlatform {
|
|||||||
return 'Flutter Native'; // 所有加密邏輯現在都在 Flutter 端執行
|
return 'Flutter Native'; // 所有加密邏輯現在都在 Flutter 端執行
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
|||||||
/// REST API 客户端
|
/// REST API 客户端
|
||||||
/// 基于 Dio,提供请求执行入口
|
/// 基于 Dio,提供请求执行入口
|
||||||
///
|
///
|
||||||
/// 拦截器链顺序:Auth → Encryption → 自定义 → Retry → Logging
|
/// 拦截器链顺序(onRequest):Auth → 自定义 → Retry → Logging → Encryption
|
||||||
|
///
|
||||||
|
/// Dio 的 onResponse / onError 按 **逆序** 执行,因此实际响应处理为:
|
||||||
|
/// `Encryption(解密) → Logging → Retry(业务码判断) → 自定义 → Auth`
|
||||||
|
///
|
||||||
|
/// EncryptionInterceptor 放最后,保证:
|
||||||
|
/// - onRequest 最后加密(其他拦截器操作明文)
|
||||||
|
/// - onResponse 最先解密(其他拦截器看到明文,业务码判断正常工作)
|
||||||
///
|
///
|
||||||
/// 使用方式:
|
/// 使用方式:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -31,13 +38,15 @@ class ApiClient {
|
|||||||
receiveTimeout: const Duration(seconds: 60),
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 挂载拦截器(顺序:Auth → Encryption → 自定义 → Retry → Logging)
|
// 挂载拦截器
|
||||||
|
// onRequest 顺序:Auth → 自定义 → Retry → Logging → Encryption
|
||||||
|
// onResponse 逆序:Encryption(解密) → Logging → Retry(业务码) → 自定义 → Auth
|
||||||
_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),
|
||||||
|
EncryptionInterceptor(config),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
|||||||
|
|
||||||
/// 加密拦截器(预留给 cipher_guard_sdk)
|
/// 加密拦截器(预留给 cipher_guard_sdk)
|
||||||
///
|
///
|
||||||
/// 在拦截器链中位于 Auth 之后、Retry 之前:
|
/// 在拦截器链中位于最末位:
|
||||||
/// `Auth → Encryption → Custom → Retry → Logging`
|
/// onRequest 顺序:`Auth → Custom → Retry → Logging → Encryption`
|
||||||
|
/// onResponse 逆序:`Encryption(解密) → Logging → Retry(业务码) → Custom → Auth`
|
||||||
|
///
|
||||||
|
/// 放最后是因为 Dio onResponse 按逆序执行——加密拦截器最先解密,
|
||||||
|
/// 后续的 RetryInterceptor 才能正确判断业务错误码、Token 过期等。
|
||||||
///
|
///
|
||||||
/// 回调为 null 时自动跳过,不影响正常请求流程。
|
/// 回调为 null 时自动跳过,不影响正常请求流程。
|
||||||
/// 后续 cipher_guard_sdk 接入后,App 层在 ApiConfig 中注入
|
/// 后续 cipher_guard_sdk 接入后,App 层在 ApiConfig 中注入
|
||||||
@@ -20,7 +24,23 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
|||||||
///
|
///
|
||||||
/// 加密回调接收原始 path、headers、body,返回 [EncryptedRequest],
|
/// 加密回调接收原始 path、headers、body,返回 [EncryptedRequest],
|
||||||
/// 拦截器根据非 null 字段覆盖请求。
|
/// 拦截器根据非 null 字段覆盖请求。
|
||||||
|
///
|
||||||
|
/// ## Token 重试与重新加密
|
||||||
|
///
|
||||||
|
/// 瞬态错误重试(5xx / 超时):复用已加密的请求,不重复加密。
|
||||||
|
/// Token 刷新重试:加密 headers 可能包含过期 token 衍生值
|
||||||
|
/// (如 X-Token、X-Signature),需要恢复加密前状态并用新 token 重新加密。
|
||||||
|
/// RetryInterceptor 通过 `_needsReEncryption` 标记通知本拦截器重新加密。
|
||||||
class EncryptionInterceptor extends Interceptor {
|
class EncryptionInterceptor extends Interceptor {
|
||||||
|
/// extra 标记键:请求已加密,瞬态重试时跳过
|
||||||
|
static const _encryptedKey = '__encrypted__';
|
||||||
|
|
||||||
|
/// extra 标记键:加密前的请求快照(path / body / contentType)
|
||||||
|
static const _preEncryptSnapshotKey = '__preEncryptSnapshot__';
|
||||||
|
|
||||||
|
/// extra 标记键:加密回调注入的 header key 列表
|
||||||
|
static const _encryptionAddedHeadersKey = '__encryptionAddedHeaders__';
|
||||||
|
|
||||||
final ApiConfig _config;
|
final ApiConfig _config;
|
||||||
|
|
||||||
EncryptionInterceptor(this._config);
|
EncryptionInterceptor(this._config);
|
||||||
@@ -36,7 +56,28 @@ class EncryptionInterceptor extends Interceptor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token 重试 + 已加密 → 恢复加密前状态,用新 token 上下文重新加密
|
||||||
|
// 旧的加密 headers(如 X-Token、X-Signature)可能包含过期 token 信息
|
||||||
|
if (options.extra[_encryptedKey] == true &&
|
||||||
|
options.extra['_needsReEncryption'] == true) {
|
||||||
|
_restorePreEncryptState(options);
|
||||||
|
options.extra.remove('_needsReEncryption');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已加密(瞬态错误重试)→ 复用加密请求,不重复加密
|
||||||
|
if (options.extra[_encryptedKey] == true) {
|
||||||
|
handler.next(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 保存加密前快照,Token 重试时恢复
|
||||||
|
options.extra[_preEncryptSnapshotKey] = {
|
||||||
|
'path': options.path,
|
||||||
|
'data': options.data,
|
||||||
|
'contentType': options.contentType,
|
||||||
|
};
|
||||||
|
|
||||||
// 收集当前 headers(转为 Map<String, String>)
|
// 收集当前 headers(转为 Map<String, String>)
|
||||||
final currentHeaders = <String, String>{};
|
final currentHeaders = <String, String>{};
|
||||||
options.headers.forEach((key, value) {
|
options.headers.forEach((key, value) {
|
||||||
@@ -54,11 +95,17 @@ class EncryptionInterceptor extends Interceptor {
|
|||||||
}
|
}
|
||||||
if (result.headers != null) {
|
if (result.headers != null) {
|
||||||
options.headers.addAll(result.headers!);
|
options.headers.addAll(result.headers!);
|
||||||
|
// 记录加密注入的 header key,Token 重试时移除
|
||||||
|
options.extra[_encryptionAddedHeadersKey] = result.headers!.keys
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
if (result.contentType != null) {
|
if (result.contentType != null) {
|
||||||
options.contentType = result.contentType;
|
options.contentType = result.contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标记已加密,防止瞬态重试时重复加密
|
||||||
|
options.extra[_encryptedKey] = true;
|
||||||
|
|
||||||
_config.onLog?.call(
|
_config.onLog?.call(
|
||||||
'Request encrypted: ${options.path}',
|
'Request encrypted: ${options.path}',
|
||||||
tag: 'Encryption',
|
tag: 'Encryption',
|
||||||
@@ -76,6 +123,41 @@ class EncryptionInterceptor extends Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 恢复加密前的请求状态
|
||||||
|
///
|
||||||
|
/// Token 重试场景:旧的加密数据(path / body / headers)可能包含过期 token,
|
||||||
|
/// 需要恢复原始状态后用新 token 上下文重新加密。
|
||||||
|
///
|
||||||
|
/// AuthInterceptor 已在本轮重试中注入了新 token headers,
|
||||||
|
/// 这里只需移除上次加密注入的 headers(如 X-Token、X-Signature),
|
||||||
|
/// 保留 Auth 设置的新 token。
|
||||||
|
void _restorePreEncryptState(RequestOptions options) {
|
||||||
|
final snapshot =
|
||||||
|
options.extra[_preEncryptSnapshotKey] as Map<String, dynamic>?;
|
||||||
|
if (snapshot != null) {
|
||||||
|
options.path = snapshot['path'] as String;
|
||||||
|
options.data = snapshot['data'];
|
||||||
|
options.contentType = snapshot['contentType'] as String?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除上次加密注入的 headers
|
||||||
|
final addedHeaders = options.extra[_encryptionAddedHeadersKey] as List?;
|
||||||
|
if (addedHeaders != null) {
|
||||||
|
for (final key in addedHeaders) {
|
||||||
|
options.headers.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除加密标记和快照
|
||||||
|
options.extra.remove(_encryptedKey);
|
||||||
|
options.extra.remove(_encryptionAddedHeadersKey);
|
||||||
|
|
||||||
|
_config.onLog?.call(
|
||||||
|
'Pre-encrypt state restored for token retry',
|
||||||
|
tag: 'Encryption',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onResponse(Response response, ResponseInterceptorHandler handler) async {
|
void onResponse(Response response, ResponseInterceptorHandler handler) async {
|
||||||
final decrypt = _config.onDecryptResponse;
|
final decrypt = _config.onDecryptResponse;
|
||||||
@@ -90,6 +172,14 @@ class EncryptionInterceptor extends Interceptor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 响应已是 Map → 未加密(health check、非加密端点等),跳过解密
|
||||||
|
// 加密模式下响应通常是 String(base64)或 List<int>(bytes)
|
||||||
|
// TODO: 接入加密后,若服务端所有端点都加密,可移除此判断
|
||||||
|
if (response.data is Map<String, dynamic>) {
|
||||||
|
handler.next(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final decrypted = await decrypt(response.data as Object);
|
final decrypted = await decrypt(response.data as Object);
|
||||||
response.data = decrypted;
|
response.data = decrypted;
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ class RetryInterceptor extends Interceptor {
|
|||||||
if (code != 0 && config.onBusinessError != null) {
|
if (code != 0 && config.onBusinessError != null) {
|
||||||
final handled = config.onBusinessError!(code, message, requestPath);
|
final handled = config.onBusinessError!(code, message, requestPath);
|
||||||
if (handled) {
|
if (handled) {
|
||||||
// App 层已处理,正常传递响应
|
// App 层已处理 → 标记,让 decodeResponse 跳过二次抛错
|
||||||
|
response.requestOptions.extra['_businessErrorHandled'] = true;
|
||||||
handler.next(response);
|
handler.next(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -115,6 +116,9 @@ class RetryInterceptor extends Interceptor {
|
|||||||
options.headers['token'] = newToken;
|
options.headers['token'] = newToken;
|
||||||
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
|
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
|
||||||
options.extra['_isTokenRetry'] = true;
|
options.extra['_isTokenRetry'] = true;
|
||||||
|
// 通知 EncryptionInterceptor:token 变了,需要用新 token 上下文重新加密
|
||||||
|
// 旧的加密 headers(如 X-Token、X-Signature)可能包含过期 token 信息
|
||||||
|
options.extra['_needsReEncryption'] = true;
|
||||||
|
|
||||||
final retryResponse = await dio.fetch(options);
|
final retryResponse = await dio.fetch(options);
|
||||||
handler.resolve(retryResponse);
|
handler.resolve(retryResponse);
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ class NetworksSdkMethodChannelDataSource {
|
|||||||
///
|
///
|
||||||
/// Dio.download 内部用 FileMode.write(从头覆盖),无法正确续传。
|
/// Dio.download 内部用 FileMode.write(从头覆盖),无法正确续传。
|
||||||
/// 这里手动读流并追加写入文件。
|
/// 这里手动读流并追加写入文件。
|
||||||
|
///
|
||||||
|
/// 如果服务端不支持 Range 请求(返回 200 而非 206),
|
||||||
|
/// 自动回退为覆盖写入,防止文件损坏。
|
||||||
Future<void> _downloadWithResume({
|
Future<void> _downloadWithResume({
|
||||||
required String url,
|
required String url,
|
||||||
required String savePath,
|
required String savePath,
|
||||||
@@ -215,14 +218,33 @@ class NetworksSdkMethodChannelDataSource {
|
|||||||
final stream = response.data?.stream;
|
final stream = response.data?.stream;
|
||||||
if (stream == null) return;
|
if (stream == null) return;
|
||||||
|
|
||||||
// Content-Length 是本次传输量(不含已下载部分)
|
// 检查服务端是否接受了 Range 请求
|
||||||
|
// 206 = 支持续传,追加写入
|
||||||
|
// 200 = 不支持 Range,返回完整文件,需要覆盖写入
|
||||||
|
final isPartialContent = response.statusCode == 206;
|
||||||
|
final effectiveStartBytes = isPartialContent ? startBytes : 0;
|
||||||
|
|
||||||
|
if (!isPartialContent) {
|
||||||
|
apiClient.config.onLog?.call(
|
||||||
|
'Server does not support Range, falling back to full download',
|
||||||
|
tag: 'Download',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-Length 是本次传输量
|
||||||
final contentLength =
|
final contentLength =
|
||||||
int.tryParse(response.headers.value('content-length') ?? '') ?? -1;
|
int.tryParse(response.headers.value('content-length') ?? '') ?? -1;
|
||||||
final totalBytes = contentLength > 0 ? contentLength + startBytes : -1;
|
final totalBytes = contentLength > 0
|
||||||
|
? contentLength + effectiveStartBytes
|
||||||
|
: -1;
|
||||||
|
|
||||||
final file = File(savePath);
|
final file = File(savePath);
|
||||||
final raf = file.openSync(mode: FileMode.writeOnlyAppend);
|
// 不支持续传时用 write 覆盖,支持时用 append 追加
|
||||||
int received = startBytes;
|
final fileMode = isPartialContent
|
||||||
|
? FileMode.writeOnlyAppend
|
||||||
|
: FileMode.writeOnly;
|
||||||
|
final raf = file.openSync(mode: fileMode);
|
||||||
|
int received = effectiveStartBytes;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await for (final chunk in stream) {
|
await for (final chunk in stream) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
|||||||
/// - Stream 输出(JSON 消息、原始字符串、二进制、连接状态、错误)
|
/// - Stream 输出(JSON 消息、原始字符串、二进制、连接状态、错误)
|
||||||
/// - 生命周期感知(前后台切换)
|
/// - 生命周期感知(前后台切换)
|
||||||
/// - Token 热更新(不断连)
|
/// - Token 热更新(不断连)
|
||||||
/// - 消息加密/解密钩子(预留给 cipher_guard_sdk)
|
/// - 消息加密/解密钩子(预留给 cipher_guard_sdk,ping/pong 走明文不加密)
|
||||||
///
|
///
|
||||||
/// ## 使用方式
|
/// ## 使用方式
|
||||||
///
|
///
|
||||||
@@ -60,6 +60,15 @@ class SocketClient {
|
|||||||
Timer? _reconnectTimer;
|
Timer? _reconnectTimer;
|
||||||
final _random = Random();
|
final _random = Random();
|
||||||
|
|
||||||
|
// ── 消息处理 ──
|
||||||
|
|
||||||
|
/// 异步消息处理链,保证解密场景下消息按到达顺序处理
|
||||||
|
///
|
||||||
|
/// 无解密回调时不使用(同步处理,天然有序)。
|
||||||
|
/// 有解密回调时,每条消息的处理链在前一条之后执行,
|
||||||
|
/// 即使解密耗时不同也不会乱序。
|
||||||
|
Future<void>? _messageProcessingChain;
|
||||||
|
|
||||||
// ── 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();
|
||||||
@@ -324,47 +333,75 @@ class SocketClient {
|
|||||||
// 内部 — 消息处理
|
// 内部 — 消息处理
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void _handleMessage(dynamic data) async {
|
void _handleMessage(dynamic data) {
|
||||||
// 二进制消息
|
// 二进制消息不需要解密,直接分发
|
||||||
if (data is List<int>) {
|
if (data is List<int>) {
|
||||||
|
if (!_binaryMessageController.isClosed) {
|
||||||
_binaryMessageController.add(
|
_binaryMessageController.add(
|
||||||
data is Uint8List ? data : Uint8List.fromList(data),
|
data is Uint8List ? data : Uint8List.fromList(data),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data is! String) {
|
if (data is! String) {
|
||||||
|
if (!_rawMessageController.isClosed) {
|
||||||
_rawMessageController.add(data.toString());
|
_rawMessageController.add(data.toString());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密(如果配置了解密回调)
|
// pong 是传输层心跳,不经过业务加解密,直接匹配
|
||||||
String text = data;
|
if (data == 'pong') {
|
||||||
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 解析
|
if (config.onDecryptMessage != null) {
|
||||||
|
// 有解密回调 → 链式异步处理,保证消息按到达顺序分发
|
||||||
|
// 避免解密耗时不同导致后到的消息先完成解密、先分发
|
||||||
|
final previous = _messageProcessingChain ?? Future.value();
|
||||||
|
_messageProcessingChain = previous.then((_) => _processTextMessage(data));
|
||||||
|
} else {
|
||||||
|
// 无解密回调 → 同步处理,天然有序
|
||||||
|
_dispatchTextMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步处理文本消息(解密 → 分发)
|
||||||
|
Future<void> _processTextMessage(String data) async {
|
||||||
|
// dispose 期间可能有残留的链式任务,直接跳过
|
||||||
|
if (_messageController.isClosed) return;
|
||||||
|
|
||||||
|
String text;
|
||||||
|
try {
|
||||||
|
text = await config.onDecryptMessage!(data);
|
||||||
|
} catch (e) {
|
||||||
|
_log('Message decryption failed: $e');
|
||||||
|
if (!_rawMessageController.isClosed) {
|
||||||
|
_rawMessageController.add(data);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_dispatchTextMessage(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分发文本消息(JSON 解析 → 投递到对应 stream)
|
||||||
|
///
|
||||||
|
/// pong 已在 `_handleMessage` 中提前拦截,不会到这里。
|
||||||
|
void _dispatchTextMessage(String text) {
|
||||||
try {
|
try {
|
||||||
final json = jsonDecode(text) as Map<String, dynamic>;
|
final json = jsonDecode(text) as Map<String, dynamic>;
|
||||||
|
if (!_messageController.isClosed) {
|
||||||
_messageController.add(json);
|
_messageController.add(json);
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// JSON 解析失败,走原始消息流
|
// JSON 解析失败,走原始消息流
|
||||||
|
if (!_rawMessageController.isClosed) {
|
||||||
_rawMessageController.add(text);
|
_rawMessageController.add(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleError(Object error) {
|
void _handleError(Object error) {
|
||||||
_log('Stream error: $error');
|
_log('Stream error: $error');
|
||||||
@@ -401,26 +438,20 @@ class SocketClient {
|
|||||||
_waitingForPong = false;
|
_waitingForPong = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendPing() async {
|
void _sendPing() {
|
||||||
if (_waitingForPong) return;
|
if (_waitingForPong) return;
|
||||||
|
|
||||||
_waitingForPong = true;
|
_waitingForPong = true;
|
||||||
|
|
||||||
// 加密场景下 ping 也要加密,与 pong 解密对称
|
// ping/pong 是传输层心跳,不经过业务加解密
|
||||||
String pingPayload = 'ping';
|
// 保证即使加密密钥过期/轮换失败,心跳仍然正常工作
|
||||||
if (config.onEncryptMessage != null) {
|
_channel?.sink.add('ping');
|
||||||
try {
|
_log('♥ ping');
|
||||||
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, () {
|
||||||
if (_waitingForPong) {
|
if (_waitingForPong) {
|
||||||
_log('Pong timeout, reconnecting...');
|
_log('♥ pong timeout, reconnecting...');
|
||||||
_waitingForPong = false;
|
_waitingForPong = false;
|
||||||
_emitError(const SocketError.pingTimeout());
|
_emitError(const SocketError.pingTimeout());
|
||||||
_doDisconnect(reason: 'Pong timeout');
|
_doDisconnect(reason: 'Pong timeout');
|
||||||
@@ -430,6 +461,7 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onPongReceived() {
|
void _onPongReceived() {
|
||||||
|
_log('♥ pong');
|
||||||
_waitingForPong = false;
|
_waitingForPong = false;
|
||||||
_pongTimeoutTimer?.cancel();
|
_pongTimeoutTimer?.cancel();
|
||||||
_pongTimeoutTimer = null;
|
_pongTimeoutTimer = null;
|
||||||
@@ -488,7 +520,9 @@ class SocketClient {
|
|||||||
try {
|
try {
|
||||||
await config.onBeforeReconnect!();
|
await config.onBeforeReconnect!();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log('onBeforeReconnect failed: $e, skip this reconnect');
|
_log('onBeforeReconnect failed: $e, schedule next reconnect');
|
||||||
|
// 重置状态以允许下次 _startReconnect 进入(防止卡死在 reconnecting)
|
||||||
|
_updateConnectionState(SocketConnectionState.disconnected);
|
||||||
_startReconnect();
|
_startReconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,36 @@ 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/domain/entities/http_method.dart';
|
import 'package:networks_sdk/src/domain/entities/http_method.dart';
|
||||||
|
|
||||||
|
|
||||||
/// API 请求基类
|
/// API 请求基类
|
||||||
///
|
///
|
||||||
/// 使用侧只需:字段 + path + method,其余全部有默认实现。
|
/// 只需 `@ApiRequest` 一个注解,声明字段和构造函数即可:
|
||||||
|
/// - `toJson()` 由 mixin 自动生成(只序列化类自身字段,不含继承属性)
|
||||||
|
/// - `path / method / requestType / includeToken` 由 mixin 自动提供
|
||||||
|
/// - Response 的 fromJson 在 mixin 的 `parameters` getter 中自动注册
|
||||||
|
/// - **不需要** `@JsonSerializable`,**不需要** 手写 `fromJson`
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// @JsonSerializable()
|
/// @ApiRequest(
|
||||||
/// class LoginRequest extends ApiRequestable<LoginData> {
|
/// path: ApiPaths.authLogin,
|
||||||
|
/// method: HttpMethod.post,
|
||||||
|
/// responseType: LoginData,
|
||||||
|
/// requestType: ApiRequestType.login,
|
||||||
|
/// )
|
||||||
|
/// class LoginRequest extends ApiRequestable<LoginData>
|
||||||
|
/// with _$LoginRequestApi {
|
||||||
/// final String email;
|
/// final String email;
|
||||||
/// final String password;
|
/// final String password;
|
||||||
///
|
///
|
||||||
/// LoginRequest({required this.email, required this.password});
|
/// LoginRequest({required this.email, required this.password});
|
||||||
///
|
/// // 完毕!一行样板代码都不用写
|
||||||
/// factory LoginRequest.fromJson(Map<String, dynamic> json) =>
|
|
||||||
/// _$LoginRequestFromJson(json);
|
|
||||||
/// @override
|
|
||||||
/// Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// String get path => '/auth/login';
|
|
||||||
/// @override
|
|
||||||
/// HttpMethod get method => HttpMethod.post;
|
|
||||||
/// @override
|
|
||||||
/// bool get includeToken => false;
|
|
||||||
/// }
|
/// }
|
||||||
///
|
|
||||||
/// // 文件顶层注册一次(一行)
|
|
||||||
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
|
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// 字段名映射:在字段上加 `@JsonKey(name: 'server_name')` 即可,
|
||||||
|
/// 生成器会读取并使用该名称作为 JSON 键。
|
||||||
|
///
|
||||||
|
/// 特殊请求(如 upload):在类中 override `toJson()` 即可,
|
||||||
|
/// 类的 override 优先于 mixin。
|
||||||
abstract class ApiRequestable<T> {
|
abstract class ApiRequestable<T> {
|
||||||
/// API 路径(如 '/auth/login')
|
/// API 路径(如 '/auth/login')
|
||||||
String get path;
|
String get path;
|
||||||
@@ -40,8 +41,8 @@ abstract class ApiRequestable<T> {
|
|||||||
/// HTTP 方法
|
/// HTTP 方法
|
||||||
HttpMethod get method;
|
HttpMethod get method;
|
||||||
|
|
||||||
/// 序列化为 JSON(由 @JsonSerializable 自动生成)
|
/// 序列化为 JSON(由 @ApiRequest 生成器在 mixin 中自动生成)
|
||||||
/// 子类 override 返回 `_$XxxToJson(this)` 即可
|
/// Upload 等特殊请求可在类中 override 返回空 map
|
||||||
Map<String, dynamic> toJson();
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
/// 请求参数 — 默认调用 toJson(),upload 类型返回 null
|
/// 请求参数 — 默认调用 toJson(),upload 类型返回 null
|
||||||
@@ -106,13 +107,17 @@ abstract class ApiRequestable<T> {
|
|||||||
final mapFunc = fromJsonFunc as T Function(Map<String, dynamic>);
|
final mapFunc = fromJsonFunc as T Function(Map<String, dynamic>);
|
||||||
return mapFunc(json);
|
return mapFunc(json);
|
||||||
}
|
}
|
||||||
throw FormatException('Expected Map<String, dynamic>, got ${json.runtimeType}',);
|
throw FormatException(
|
||||||
|
'Expected Map<String, dynamic>, got ${json.runtimeType}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final wrapper = ApiResponseWrapper<T>.fromJson(data, fromJsonObject);
|
final wrapper = ApiResponseWrapper<T>.fromJson(data, fromJsonObject);
|
||||||
|
|
||||||
// 业务错误码检查
|
// 业务错误码检查(RetryInterceptor 已处理的跳过,防止双重抛错)
|
||||||
if (wrapper.code != 0) {
|
final handledByInterceptor =
|
||||||
|
response.requestOptions.extra['_businessErrorHandled'] == true;
|
||||||
|
if (wrapper.code != 0 && !handledByInterceptor) {
|
||||||
throw ApiError.apiError(
|
throw ApiError.apiError(
|
||||||
code: wrapper.code,
|
code: wrapper.code,
|
||||||
message: wrapper.message ?? 'API error (code: ${wrapper.code})',
|
message: wrapper.message ?? 'API error (code: ${wrapper.code})',
|
||||||
@@ -141,8 +146,9 @@ final fromJsonRegistry = <Type, Function>{};
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
|
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
|
||||||
/// ```
|
/// ```
|
||||||
T Function(Map<String, dynamic>)? registerResponse<T>(T Function(Map<String, dynamic>) fromJson,)
|
T Function(Map<String, dynamic>)? registerResponse<T>(
|
||||||
{
|
T Function(Map<String, dynamic>) fromJson,
|
||||||
|
) {
|
||||||
fromJsonRegistry[T] = fromJson;
|
fromJsonRegistry[T] = fromJson;
|
||||||
return fromJson;
|
return fromJson;
|
||||||
}
|
}
|
||||||
@@ -152,9 +158,7 @@ T Function(Map<String, dynamic>)? registerResponse<T>(T Function(Map<String, dyn
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// final _reg = registerResponseObject<DeviceList>(DeviceList.fromJson);
|
/// final _reg = registerResponseObject<DeviceList>(DeviceList.fromJson);
|
||||||
/// ```
|
/// ```
|
||||||
T Function(Object?)? registerResponseObject<T>(
|
T Function(Object?)? registerResponseObject<T>(T Function(Object?) fromJson) {
|
||||||
T Function(Object?) fromJson,
|
|
||||||
) {
|
|
||||||
fromJsonRegistry[T] = fromJson;
|
fromJsonRegistry[T] = fromJson;
|
||||||
return fromJson;
|
return fromJson;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
/// API 响应信封解析器
|
/// API 响应包装解析器
|
||||||
/// 统一处理 { code, message/msg, data } 格式的服务器响应
|
/// 统一处理 { code, message/msg, data } 格式的服务器响应
|
||||||
class ApiResponseWrapper<T> {
|
class ApiResponseWrapper<T> {
|
||||||
final int code;
|
final int code;
|
||||||
final String? message;
|
final String? message;
|
||||||
final T? data;
|
final T? data;
|
||||||
|
|
||||||
const ApiResponseWrapper({
|
const ApiResponseWrapper({required this.code, this.message, this.data});
|
||||||
required this.code,
|
|
||||||
this.message,
|
|
||||||
this.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ApiResponseWrapper.fromJson(
|
factory ApiResponseWrapper.fromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
@@ -28,8 +24,7 @@ class ApiResponseWrapper<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// message 字段:兼容 message 和 msg
|
// message 字段:兼容 message 和 msg
|
||||||
final message =
|
final message = json['message'] as String? ?? json['msg'] as String?;
|
||||||
json['message'] as String? ?? json['msg'] as String?;
|
|
||||||
|
|
||||||
// 解码 data(null-safe:logout / delete 等接口可能无 data)
|
// 解码 data(null-safe:logout / delete 等接口可能无 data)
|
||||||
final rawData = json['data'];
|
final rawData = json['data'];
|
||||||
|
|||||||
@@ -4,17 +4,50 @@ import 'package:networks_sdk/src/annotations/api_request.dart';
|
|||||||
import 'package:source_gen/source_gen.dart';
|
import 'package:source_gen/source_gen.dart';
|
||||||
import 'package:build/build.dart';
|
import 'package:build/build.dart';
|
||||||
|
|
||||||
|
/// @JsonKey 检测器(用于读取字段的 JSON 键名映射)
|
||||||
|
const _jsonKeyChecker = TypeChecker.fromUrl(
|
||||||
|
'package:json_annotation/src/json_key.dart#JsonKey',
|
||||||
|
);
|
||||||
|
|
||||||
/// @ApiRequest 代码生成器
|
/// @ApiRequest 代码生成器
|
||||||
///
|
///
|
||||||
/// 为标注了 `@ApiRequest` 的类自动生成 mixin,提供:
|
/// 为标注了 `@ApiRequest` 的类自动生成 mixin,提供:
|
||||||
/// - `path`, `method`, `requestType`, `includeToken` 协议实现
|
/// - `path`, `method`, `requestType`, `includeToken` 协议实现
|
||||||
/// - 自动注册响应类型的 `fromJson`(在 `parameters` getter 中触发,
|
/// - `toJson()` — 从类的声明字段自动生成,只序列化自身字段,
|
||||||
/// 保证首次请求前完成注册,无需手动调用 `registerApiResponses()`)
|
/// 不含 ApiRequestable 的继承属性,避免递归
|
||||||
|
/// - 自动注册响应类型的 `fromJson`(在 `parameters` getter 中触发)
|
||||||
///
|
///
|
||||||
/// 生成的 mixin 命名规则:`_$<ClassName>Api`
|
/// 支持 `@JsonKey(name: '...')` 字段重命名。
|
||||||
|
/// 如有 `@JsonKey(includeToJson: false)` 则跳过该字段。
|
||||||
|
///
|
||||||
|
/// ## 使用模式
|
||||||
|
///
|
||||||
|
/// Request 类只需 `@ApiRequest` 注解,无需 `@JsonSerializable`:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// @ApiRequest(
|
||||||
|
/// path: ApiPaths.authLogin,
|
||||||
|
/// method: HttpMethod.post,
|
||||||
|
/// responseType: LoginData,
|
||||||
|
/// requestType: ApiRequestType.login,
|
||||||
|
/// )
|
||||||
|
/// class LoginRequest extends ApiRequestable<LoginData>
|
||||||
|
/// with _$LoginRequestApi {
|
||||||
|
/// final String email;
|
||||||
|
/// final String password;
|
||||||
|
///
|
||||||
|
/// LoginRequest({required this.email, required this.password});
|
||||||
|
/// // 完毕!toJson / path / method 全部由 mixin 自动生成
|
||||||
|
/// // Response 的 fromJson 在 parameters getter 中自动注册
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## mixin 命名规则
|
||||||
|
///
|
||||||
|
/// `_$<ClassName>Api`
|
||||||
|
///
|
||||||
|
/// ## 生成示例
|
||||||
///
|
///
|
||||||
/// 示例输出:
|
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// mixin _$LoginRequestApi on ApiRequestable<LoginData> {
|
/// mixin _$LoginRequestApi on ApiRequestable<LoginData> {
|
||||||
/// @override String get path => '/auth/login';
|
/// @override String get path => '/auth/login';
|
||||||
@@ -22,17 +55,29 @@ import 'package:build/build.dart';
|
|||||||
/// @override ApiRequestType get requestType => ApiRequestType.login;
|
/// @override ApiRequestType get requestType => ApiRequestType.login;
|
||||||
/// @override bool get includeToken => false;
|
/// @override bool get includeToken => false;
|
||||||
/// @override
|
/// @override
|
||||||
|
/// Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
|
/// 'email': (this as LoginRequest).email,
|
||||||
|
/// 'password': (this as LoginRequest).password,
|
||||||
|
/// };
|
||||||
|
/// @override
|
||||||
/// Map<String, dynamic>? get parameters {
|
/// Map<String, dynamic>? get parameters {
|
||||||
/// registerResponse<LoginData>(LoginData.fromJson);
|
/// registerResponse<LoginData>(LoginData.fromJson);
|
||||||
/// return super.parameters;
|
/// return super.parameters;
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
|
///
|
||||||
{
|
/// ## Upload 等特殊请求
|
||||||
|
///
|
||||||
|
/// 如需自定义 toJson(如 upload 返回空 map),在类中 override 即可,
|
||||||
|
/// 类的 override 优先于 mixin。
|
||||||
|
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
|
||||||
@override
|
@override
|
||||||
String generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep,)
|
String generateForAnnotatedElement(
|
||||||
{
|
Element element,
|
||||||
|
ConstantReader annotation,
|
||||||
|
BuildStep buildStep,
|
||||||
|
) {
|
||||||
if (element is! ClassElement) {
|
if (element is! ClassElement) {
|
||||||
throw InvalidGenerationSourceError(
|
throw InvalidGenerationSourceError(
|
||||||
'@ApiRequest can only be applied to classes.',
|
'@ApiRequest can only be applied to classes.',
|
||||||
@@ -40,7 +85,7 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final className = element.name;
|
final className = element.name!;
|
||||||
final path = annotation.read('path').stringValue;
|
final path = annotation.read('path').stringValue;
|
||||||
|
|
||||||
// 读取 HttpMethod 枚举值
|
// 读取 HttpMethod 枚举值
|
||||||
@@ -68,6 +113,9 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
|
|||||||
includeToken = requestTypeName != 'login';
|
includeToken = requestTypeName != 'login';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从类的声明字段生成 toJson()
|
||||||
|
final toJsonBody = _buildToJsonBody(element, className);
|
||||||
|
|
||||||
return '''
|
return '''
|
||||||
/// Generated by @ApiRequest for [$className]
|
/// Generated by @ApiRequest for [$className]
|
||||||
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
||||||
@@ -80,6 +128,8 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
|||||||
@override
|
@override
|
||||||
bool get includeToken => $includeToken;
|
bool get includeToken => $includeToken;
|
||||||
@override
|
@override
|
||||||
|
Map<String, dynamic> toJson() => $toJsonBody;
|
||||||
|
@override
|
||||||
Map<String, dynamic>? get parameters {
|
Map<String, dynamic>? get parameters {
|
||||||
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
||||||
return super.parameters;
|
return super.parameters;
|
||||||
@@ -88,6 +138,46 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
|||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从类的声明字段构建 toJson() 方法体
|
||||||
|
///
|
||||||
|
/// 只读取类自身声明的实例字段(非 static、非 synthetic),
|
||||||
|
/// 不含继承自 ApiRequestable 的属性,避免递归。
|
||||||
|
/// 支持 @JsonKey(name: '...') 字段重命名,
|
||||||
|
/// 以及 @JsonKey(includeToJson: false) 跳过字段。
|
||||||
|
String _buildToJsonBody(ClassElement element, String className) {
|
||||||
|
final fields = element.fields
|
||||||
|
.where((f) => !f.isStatic && !f.isSynthetic)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (fields.isEmpty) {
|
||||||
|
return '<String, dynamic>{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
final entries = <String>[];
|
||||||
|
for (final field in fields) {
|
||||||
|
// 检查 @JsonKey 注解
|
||||||
|
final jsonKeyAnnotation = _jsonKeyChecker.firstAnnotationOfExact(field);
|
||||||
|
|
||||||
|
// @JsonKey(includeToJson: false) → 跳过
|
||||||
|
final includeToJson = jsonKeyAnnotation
|
||||||
|
?.getField('includeToJson')
|
||||||
|
?.toBoolValue();
|
||||||
|
if (includeToJson == false) continue;
|
||||||
|
|
||||||
|
// JSON 键名:@JsonKey(name: '...') 或字段名
|
||||||
|
final jsonName =
|
||||||
|
jsonKeyAnnotation?.getField('name')?.toStringValue() ?? field.name;
|
||||||
|
|
||||||
|
entries.add("'$jsonName': (this as $className).${field.name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.isEmpty) {
|
||||||
|
return '<String, dynamic>{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<String, dynamic>{${entries.join(', ')}}';
|
||||||
|
}
|
||||||
|
|
||||||
/// 从 DartObject 提取枚举常量名称
|
/// 从 DartObject 提取枚举常量名称
|
||||||
String _readEnumName(dynamic dartObject, String defaultValue) {
|
String _readEnumName(dynamic dartObject, String defaultValue) {
|
||||||
final index = dartObject.getField('index')?.toIntValue();
|
final index = dartObject.getField('index')?.toIntValue();
|
||||||
|
|||||||
@@ -59,7 +59,15 @@ typedef OnEncryptRequest =
|
|||||||
///
|
///
|
||||||
/// [responseData] 的实际类型取决于服务端响应格式:
|
/// [responseData] 的实际类型取决于服务端响应格式:
|
||||||
/// - 加密模式下通常是 base64 字符串
|
/// - 加密模式下通常是 base64 字符串
|
||||||
/// - 非加密模式下是 `Map<String, dynamic>`
|
/// - 非加密模式下是 `Map<String, dynamic>`(拦截器会自动跳过,不调用此回调)
|
||||||
|
///
|
||||||
|
/// 实现时建议做类型判断兜底,应对非预期的响应格式:
|
||||||
|
/// ```dart
|
||||||
|
/// onDecryptResponse: (data) async {
|
||||||
|
/// if (data is! String) throw FormatException('Expected String, got ${data.runtimeType}');
|
||||||
|
/// return jsonDecode(aesDecrypt(data));
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
typedef OnDecryptResponse =
|
typedef OnDecryptResponse =
|
||||||
Future<Map<String, dynamic>> Function(Object responseData);
|
Future<Map<String, dynamic>> Function(Object responseData);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||||||
) async {
|
) async {
|
||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return;
|
if (db == null) return;
|
||||||
await db.into(table).insertOnConflictUpdate(companion);
|
await db.into(table).insert(companion, mode: InsertMode.insertOrReplace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -44,7 +44,8 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return;
|
if (db == null) return;
|
||||||
await db.batch(
|
await db.batch(
|
||||||
(batch) => batch.insertAllOnConflictUpdate(table, companions),
|
(batch) =>
|
||||||
|
batch.insertAll(table, companions, mode: InsertMode.insertOrReplace),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +107,10 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||||||
) async {
|
) async {
|
||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return null;
|
if (db == null) return null;
|
||||||
return (db.select(table)..where(filter)..limit(1)).getSingleOrNull();
|
return (db.select(table)
|
||||||
|
..where(filter)
|
||||||
|
..limit(1))
|
||||||
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
@@ -135,7 +139,10 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||||||
) {
|
) {
|
||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return const Stream.empty();
|
if (db == null) return const Stream.empty();
|
||||||
return (db.select(table)..where(filter)..limit(1)).watchSingleOrNull();
|
return (db.select(table)
|
||||||
|
..where(filter)
|
||||||
|
..limit(1))
|
||||||
|
.watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
||||||
@@ -148,18 +155,12 @@ class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return [];
|
if (db == null) return [];
|
||||||
return db
|
return db
|
||||||
.customSelect(
|
.customSelect(sql, variables: args.map((e) => Variable(e)).toList())
|
||||||
sql,
|
|
||||||
variables: args.map((e) => Variable(e)).toList(),
|
|
||||||
)
|
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> rawExecute(
|
Future<void> rawExecute(String sql, [List<Object?> args = const []]) async {
|
||||||
String sql, [
|
|
||||||
List<Object?> args = const [],
|
|
||||||
]) async {
|
|
||||||
final db = _db;
|
final db = _db;
|
||||||
if (db == null) return;
|
if (db == null) return;
|
||||||
await db.customStatement(sql, args);
|
await db.customStatement(sql, args);
|
||||||
|
|||||||
Reference in New Issue
Block a user