diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html index 8cb14bf..2793eac 100644 --- a/Doc/IM_App_架构设计.html +++ b/Doc/IM_App_架构设计.html @@ -895,7 +895,7 @@ flowchart TD │ │ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口 │ │ ├── dto/ │ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 -│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析 +│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析 │ │ └── repositories/ │ │ ├── networks_sdk_repository_impl.dart │ │ └── networks_messaging_repository_impl.dart @@ -2299,8 +2299,8 @@ class LoginData { User toEntity() => User(id: userId, email: email); // DTO → Domain Entity } -// ── Request ── -// @ApiRequest 自动生成 path / method / requestType / includeToken / fromJson 注册 +// ── Request(零样板:只需 @ApiRequest,无需 @JsonSerializable / fromJson / toJson)── +// @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册 @ApiRequest( path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理 @@ -2308,15 +2308,12 @@ class LoginData { responseType: LoginData, requestType: ApiRequestType.login, ) -@JsonSerializable() class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi { final String email; final String password; LoginRequest({required this.email, required this.password}); - - @override - Map<String, dynamic> toJson() => _$LoginRequestToJson(this); + // 完毕!toJson 由 mixin 从类字段自动生成,fromJson 不需要(Request 永远手动构造) } @@ -2353,17 +2350,17 @@ final user = loginData?.toEntity(); // DTO → Domain Entity
核心优势:
@ApiRequest 自动生成 mixin,@JsonSerializable 自动生成 toJson/fromJson@ApiRequest 一个注解自动生成 mixin(含 toJson),无需 @JsonSerializableregisterApiResponses()生成 mixin(非 extension),因为 mixin 可以 override 基类方法、调用 super,并在 parameters getter 中自动注册 fromJson。
toJson 生成机制:生成器读取类的声明字段(非继承),直接在 mixin 中生成 Map 字面量。不依赖 @JsonSerializable,避免了继承属性被序列化导致的递归问题。支持 @JsonKey(name: '...') 字段重命名和 @JsonKey(includeToJson: false) 跳过字段。
/// API 请求代码生成器
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
@override
- String generateForAnnotatedElement(
- Element element,
- ConstantReader annotation,
- BuildStep buildStep,
- ) {
+ String generateForAnnotatedElement(element, annotation, buildStep) {
final className = element.name;
- final path = annotation.read('path').stringValue;
- final methodName = _readEnumName(annotation.read('method').objectValue, 'post');
- final responseType = annotation.read('responseType').typeValue;
- final responseTypeName = responseType.getDisplayString();
- final requestTypeName = _readEnumName(annotation.read('requestType').objectValue, 'request');
+ // ... 读取 path / method / responseType / requestType / includeToken ...
- // includeToken:默认 login → false,其余 → true
- final includeTokenReader = annotation.peek('includeToken');
- final includeToken = (includeTokenReader != null && !includeTokenReader.isNull)
- ? includeTokenReader.boolValue
- : requestTypeName != 'login';
+ // 从类的声明字段生成 toJson(),只序列化自身字段,不含继承属性
+ final toJsonBody = _buildToJsonBody(element, className);
- // 生成 mixin,使用侧只需 `with _$XxxApi`
return '''
-/// Generated by @ApiRequest for [$className]
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@override String get path => '$path';
@override HttpMethod get method => HttpMethod.$methodName;
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
@override bool get includeToken => $includeToken;
@override
+ Map<String, dynamic> toJson() => $toJsonBody;
+ @override
Map<String, dynamic>? get parameters {
registerResponse<$responseTypeName>($responseTypeName.fromJson);
return super.parameters;
@@ -2484,16 +2472,28 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
}
''';
}
+
+ /// 读取类的声明字段,生成 Map 字面量
+ /// 支持 @JsonKey(name: '...') 重命名
+ String _buildToJsonBody(ClassElement element, String className) {
+ final fields = element.fields.where((f) => !f.isStatic && !f.isSynthetic);
+ // => {'email': (this as LoginRequest).email, 'password': ...}
+ }
}
-关键设计:parameters getter 在首次请求时自动调用 registerResponse,将 fromJson 注册到全局注册表。无需手动注册,也无需 registerApiResponses() 启动函数。
关键设计:
+toJson() 只序列化类自身声明的字段,不含 ApiRequestable 的继承属性(path / method / parameters 等),避免递归parameters getter 在首次请求时自动调用 registerResponse,将 Response 的 fromJson 注册到全局注册表toJson(),类的 override 优先于 mixin文件:packages/networks_sdk/build.yaml
使用 SharedPartBuilder,与 @JsonSerializable 共享同一个 .g.dart 文件,无需额外 part 指令。
使用 SharedPartBuilder,与 @JsonSerializable(Response DTO 用)共享同一个 .g.dart 文件,无需额外 part 指令。
builders:
api_request:
@@ -2522,12 +2522,12 @@ melos run gen
4.5 更多使用示例
-所有示例遵循同一模式:@ApiRequest + @JsonSerializable + extends ApiRequestable<T> with _$XxxApi。
+所有示例遵循同一模式:@ApiRequest + extends ApiRequestable<T> with _$XxxApi。Request 类无需 @JsonSerializable。
-发送消息请求(POST):
+发送消息请求(POST + @JsonKey 字段重命名):
// data/remote/send_message_request.dart
-// ── Response DTO ──
+// ── Response DTO(仍用 @JsonSerializable)──
@JsonSerializable()
class SendMessageData {
@JsonKey(name: 'message_id')
@@ -2539,25 +2539,23 @@ class SendMessageData {
_$SendMessageDataFromJson(json);
}
-// ── Request ──
+// ── Request(零样板)──
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
-@JsonSerializable()
class SendMessageRequest extends ApiRequestable<SendMessageData>
with _$SendMessageRequestApi {
- @JsonKey(name: 'chat_id')
+ @JsonKey(name: 'chat_id') // 生成器会读取,JSON 键名为 'chat_id'
final String chatId;
final String content;
SendMessageRequest({required this.chatId, required this.content});
- @override
- Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
+ // toJson 自动生成:{'chat_id': chatId, 'content': content}
}
获取用户资料(GET,靠 token 标识当前用户,无需传参):
// data/remote/get_profile_request.dart
-@JsonSerializable()
+@JsonSerializable(createToJson: false) // 只需反序列化
class ProfileData {
@JsonKey(name: 'user_id')
final String userId;
@@ -2573,13 +2571,9 @@ class ProfileData {
}
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
-@JsonSerializable()
class GetProfileRequest extends ApiRequestable<ProfileData>
with _$GetProfileRequestApi {
- GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
-
- @override
- Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
+ GetProfileRequest(); // 无参数 — toJson 自动生成空 map
}
@@ -2622,8 +2616,8 @@ class UploadFileRequest extends ApiRequestable<UploadResult>
核心价值
-- 极简使用:字段 + 构造函数 +
@ApiRequest + @JsonSerializable
-- 零维护:path / method / requestType / includeToken / fromJson 注册 全部自动生成
+- 极简使用:字段 + 构造函数 +
@ApiRequest(Request 无需 @JsonSerializable、无需 fromJson、无需手写 toJson)
+- 零维护:path / method / requestType / includeToken / toJson / fromJson 注册 全部自动生成
- 类型安全:泛型
ApiRequestable<T> + responseType 编译期检查
- 一个端点 = 一个文件:Response DTO + Request 放在同一文件,打开即看全貌
@@ -3211,6 +3205,7 @@ flowchart LR
两大核心逻辑:
1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。
2. Riverpod 单向数据流:用户操作 → ref.read(vm.notifier).action() → ViewModel 处理逻辑 → state = newState → ref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。
+3. Widget 纯展示原则:View(Widget)层对业务数据严格只读。所有逻辑(导航、CRUD、状态变更、条件判断)必须在 ViewModel 中完成,View 只调用 ViewModel 方法并渲染返回的 State。包括 demo/测试页面也不例外。
@@ -5698,7 +5693,7 @@ class MessageLocalDataSource {
- 一个端点 = 一个文件:Response DTO + Request 类放在同一文件中
- Repository 直接调 NetworksSdkApi:无需 RemoteDataSource 中间层
-- @ApiRequest 注解 + 代码生成:自动实现 path / method / fromJson 注册
+- @ApiRequest 注解 + 代码生成:自动实现 path / method / toJson / fromJson 注册(Request 无需 @JsonSerializable)
// 示例:Repository 直接调用 Request
@@ -5907,7 +5902,7 @@ flowchart LR
│ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口(executeRequest / executeDownload)
│ ├── dto/
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
- │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
+ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析
│ └── repositories/
│ ├── networks_sdk_repository_impl.dart
│ └── networks_messaging_repository_impl.dart
@@ -5954,7 +5949,7 @@ flowchart LR
WebSocket 连接 SocketClient 内部管理(连接/心跳/重连) 调 connect/disconnect/send
WebSocket 心跳 双层心跳自动管理(底层 ping 5s + 应用层 10s) 无需关心
WebSocket 重连 指数退避自动重连(1s→2s→4s→8s→16s→30s) 无需关心
-WebSocket 生命周期 提供 onEnterForeground/Background App 层调用(AppLifecycleListener)
+WebSocket 生命周期 提供 onEnterForeground/Background App 层调用(AppLifecycleListener)。本项目 disconnectInBackground=false,所有平台后台保活、心跳不停
WebSocket 消息解析 JSON.decode → Stream 输出 App 层按 type 过滤 + DTO 解析
Riverpod 无依赖 Provider 包装 NetworksSdkApi / SocketClient
@@ -6529,7 +6524,7 @@ class UploadFileRequest extends ApiRequestable<UploadResult>
@override
Object? get uploadData => data; // Uint8List 直接作为 body
- /// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 信封
+ /// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 响应格式
/// 必须 override decodeResponse
@override
S3UploadResponse? decodeResponse(Response response) {
@@ -6798,12 +6793,54 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));
端对端加密 SDK,同时处理 Dart 侧加解密和 Native 侧密钥同步(iOS App Group 用于推送通知解密):
cipher_guard_sdk_api.dart:公开 API 接口(Facade)
-encryption_flutter_service.dart:RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt)
+encryption_flutter_service.dart:RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt),含性能优化
encryption_method_channel.dart:Native 密钥同步通道(iOS App Group 共享密钥供 Notification Extension 解密)
- Domain 实体:
RsaKeyPair / SessionKey / EncryptedMessage / ChatEncryptionKey
android/ + ios/:Plugin 注册入口,原生侧实现密钥写入 App Group
+加解密性能优化
+
+encryption_flutter_service.dart 针对 IM 高频加解密场景做了四项优化:
+
+
+
+优化项
+方案
+效果
+
+
+
+RSA 密钥生成异步化
+generateRsaKeyPairAsync 使用 Isolate.run() 在独立线程生成
+主线程零阻塞,不卡 UI(2048-bit 约 200-500ms)
+
+
+派生密钥 LRU 缓存
+_derivedKeyCache(Map,上限 64 条),缓存键 = sessionKey:round:mode,满时淘汰最早条目
+同一 round 的加解密只算一次 KDF,后续直接命中缓存
+
+
+Random.secure() 复用
+静态 _secureRandom 单例,所有 IV / 随机数生成共用
+避免每次 Random.secure() 构造开销
+
+
+KDF 双模式
+KdfMode.md5(默认,兼容既有数据)和 KdfMode.pbkdf2(PBKDF2-HMAC-SHA256,可配迭代次数)
+默认快速兼容,可选安全增强(防暴力破解)
+
+
+
+
+构造时可配置 KDF 模式和 PBKDF2 迭代次数:
+EncryptionFlutterService(
+ kdfMode: KdfMode.md5, // 默认,兼容既有数据
+ pbkdf2Iterations: 10000, // PBKDF2 模式下的迭代次数
+)
+
+clearDerivedKeyCache() 可在 session key 轮换时手动清空缓存。
+
7.4 多语言国际化(packages/l10n_sdk/)
已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。
diff --git a/apps/im_app/assets/loginData.json b/apps/im_app/assets/loginData.json
new file mode 100644
index 0000000..222e7c1
--- /dev/null
+++ b/apps/im_app/assets/loginData.json
@@ -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": ""
+ }
+}
diff --git a/apps/im_app/lib/app/di/network_provider.dart b/apps/im_app/lib/app/di/network_provider.dart
index 92f485c..d02678e 100644
--- a/apps/im_app/lib/app/di/network_provider.dart
+++ b/apps/im_app/lib/app/di/network_provider.dart
@@ -103,7 +103,7 @@ final apiConfigProvider = Provider((ref) {
onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调
onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
onTransformResponse:
- null, // TODO: 如后端信封结构非标准,在此归一化为 { code, data, message }
+ null, // TODO: 如后端响应格式非标准,在此归一化为 { code, data, message }
onGetTokenExpiry: parseJwtExpiry,
maxRetries: AppConstants.maxRetries,
retryBaseDelay: AppConstants.retryBaseDelay,
@@ -214,6 +214,7 @@ final socketManagerProvider = Provider((ref) {
final manager = SocketManager(
client: client,
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
+ disconnectInBackground: false, // 所有平台后台保活,心跳不停、连接不断
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
onBeforeReconnect: () async {
// 重连前检查 token 是否即将过期,是则主动刷新
@@ -327,9 +328,11 @@ String _buildWsUrl(String httpBaseUrl) {
//
// WidgetsBindingObserver(App 层 app.dart)
// → SocketManager.onEnterBackground()
-// disconnectInBackground=true → disconnect(默认,移动端省电)
-// disconnectInBackground=false → 完全保活,不断连不暂停心跳(桌面端)
-// → SocketManager.onEnterForeground() → onBeforeReconnect → reconnect
+// disconnectInBackground=false → 完全保活,心跳不停(本项目默认)
+// disconnectInBackground=true → disconnect + 暂停心跳(省电模式)
+// → SocketManager.onEnterForeground()
+// 保活模式 → 检查连接健康,异常则重连
+// 断连模式 → onBeforeReconnect → reconnect
//
// Token 刷新 → WebSocket 同步链路:
//
@@ -524,5 +527,5 @@ String _buildWsUrl(String httpBaseUrl) {
// Upload B: 二进制上传到 S3 presigned URL
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
// @override Object? get uploadData => bytes; // Uint8List
-// @override decodeResponse(response) { ... } // S3 不走标准信封
+// @override decodeResponse(response) { ... } // S3 不走标准响应格式
//
diff --git a/apps/im_app/lib/core/services/socket_manager.dart b/apps/im_app/lib/core/services/socket_manager.dart
index a1128dd..efc1d8b 100644
--- a/apps/im_app/lib/core/services/socket_manager.dart
+++ b/apps/im_app/lib/core/services/socket_manager.dart
@@ -16,7 +16,7 @@ typedef MessageTransformer =
///
/// 在 SocketClient(SDK 底层能力)之上封装:
/// - 连接/断连生命周期(登录连接、登出断连)
-/// - 前后台生命周期(后台断连省电、前台自动重连)
+/// - 前后台生命周期(两种模式:后台断连 / 后台保活)
/// - 网络状态响应(断网断连、恢复网络立即重连)
/// - 操作前置检查(网络可用性 + 后台状态)
/// - 消息预处理管道(通过 [onMessageTransform] 回调注入解密等)
@@ -38,13 +38,13 @@ typedef MessageTransformer =
/// ```
/// 登录成功 → connect(token) → 前置检查 → 建立连接
///
-/// ── disconnectInBackground = true(默认,移动端)──
+/// ── disconnectInBackground = true(后台断连模式)──
/// App 进后台 → onEnterBackground() → 暂停心跳 + 断开连接(省电)
/// App 回前台 → onEnterForeground() → 恢复心跳 → onBeforeReconnect → 重连
///
-/// ── disconnectInBackground = false(桌面端)──
-/// App 进后台 → onEnterBackground() → 不操作,完全保活
-/// App 回前台 → onEnterForeground() → 不操作(连接始终在线)
+/// ── disconnectInBackground = false(后台保活模式,本项目默认)──
+/// App 进后台 → onEnterBackground() → 不操作,心跳不停、连接不断
+/// App 回前台 → onEnterForeground() → 检查连接健康,异常则重连
///
/// 网络丢失 → handleNetworkLost() → 断开连接
/// 网络恢复 → handleNetworkRestored() → 退避 → onBeforeReconnect → 重连
@@ -86,9 +86,9 @@ class SocketManager {
/// 进后台时是否断开连接
///
- /// true(默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
- /// false — 后台保持连接(适用于桌面端或需要后台实时推送的场景)。
- /// 设为 false 时,后台仅暂停心跳,不主动断连。
+ /// true(SDK 默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
+ /// false(本项目使用)— 后台保持连接,心跳不停、请求不停,最大程度保活。
+ /// 回前台时检查连接健康,异常则触发重连。
final bool disconnectInBackground;
/// 日志回调
@@ -147,7 +147,7 @@ class SocketManager {
_reconnectOnForeground = false;
_reconnectOnNetworkRestore = false;
- // 前置检查:移动端模式下在后台不连接(省电)
+ // 前置检查:后台断连模式下在后台不连接(省电)
if (_isInBackground && disconnectInBackground) {
_reconnectOnForeground = true;
_log('In background, defer connect to foreground');
@@ -200,18 +200,18 @@ class SocketManager {
// ── 前后台生命周期 ────────────────────────────────────────────────────────
//
- // 后台 → 断连(省电省流量)或保持连接(桌面端)
- // 前台 → 自动重连(如果之前有连接)
+ // 后台 → 保活(心跳不停、连接不断)或断连(省电模式)
+ // 前台 → 检查连接健康 / 自动重连
/// App 进后台
///
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
///
- /// [disconnectInBackground] 为 true 时(默认,移动端):
- /// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
- ///
- /// [disconnectInBackground] 为 false 时(桌面端):
+ /// [disconnectInBackground] 为 false 时(后台保活,本项目默认):
/// 不断连、不暂停心跳,WebSocket 完全保活。
+ ///
+ /// [disconnectInBackground] 为 true 时(后台断连模式):
+ /// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
void onEnterBackground() {
_isInBackground = true;
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
@@ -219,12 +219,12 @@ class SocketManager {
_foregroundReconnectTimer = null;
if (!disconnectInBackground) {
- // 桌面端模式:不断连、不暂停心跳,完全保活
+ // 后台保活模式:不断连、不暂停心跳,不通知 SocketClient
_log('Entering background, keeping connection alive');
return;
}
- // 移动端模式:通知 SocketClient 进后台(暂停心跳)
+ // 后台断连模式:通知 SocketClient 进后台(暂停心跳)
_client.onEnterBackground();
if (_lastToken == null) return; // 未登录,无需处理
@@ -240,50 +240,76 @@ class SocketManager {
}
}
- /// App 回前台 → 自动重连(如果之前后台断连)
+ /// App 回前台
///
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.resumed] 时调用。
- /// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
+ ///
+ /// 后台保活模式(disconnectInBackground=false):
+ /// 检查连接健康,如果后台期间连接意外断开则自动重连。
+ ///
+ /// 后台断连模式(disconnectInBackground=true):
+ /// 通知 SocketClient 恢复心跳,然后重新建立连接。
void onEnterForeground() {
_isInBackground = false;
- // 只在移动端模式(后台曾断连/暂停心跳)时通知 SocketClient 恢复
if (disconnectInBackground) {
+ // 后台断连模式:通知 SocketClient 恢复心跳
_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) {
+ // 后台断连模式:之前后台断连过,需要重连
_reconnectOnForeground = false;
_log('Returning to foreground, reconnecting...');
- // 延迟 500ms 等待网络稳定,通过 Timer 跟踪以便进后台时取消
- _foregroundReconnectTimer?.cancel();
- _foregroundReconnectTimer = Timer(
- const Duration(milliseconds: 500),
- () async {
- _foregroundReconnectTimer = null;
- // 双重保险:回调执行时再次检查后台状态
- if (_isInBackground) {
- _reconnectOnForeground = true;
- _log('Went back to background during delay, skip reconnect');
+ _scheduleReconnect();
+ }
+ }
+
+ /// 延迟 500ms 后执行重连
+ ///
+ /// 等待网络稳定,通过 Timer 跟踪以便进后台时取消。
+ void _scheduleReconnect() {
+ _foregroundReconnectTimer?.cancel();
+ _foregroundReconnectTimer = Timer(
+ const Duration(milliseconds: 500),
+ () async {
+ _foregroundReconnectTimer = null;
+ // 双重保险:回调执行时再次检查后台状态
+ if (_isInBackground) {
+ _reconnectOnForeground = true;
+ _log('Went back to background during delay, skip reconnect');
+ return;
+ }
+ if (!_client.isConnected && _lastToken != null) {
+ // 前置检查:网络可用性
+ if (!await _isNetworkAvailable()) {
+ _reconnectOnNetworkRestore = true;
+ _log('Network unavailable, defer reconnect to network restore');
return;
}
- if (!_client.isConnected && _lastToken != null) {
- // 前置检查:网络可用性
- if (!await _isNetworkAvailable()) {
- _reconnectOnNetworkRestore = true;
- _log('Network unavailable, defer reconnect to network restore');
- return;
- }
- // 重连前钩子:刷新即将过期的 token 等
- await onBeforeReconnect?.call();
- // token 可能被 onBeforeReconnect 更新(通过 updateToken 链路同步)
- if (_lastToken != null && !_client.isConnected) {
- _client.connect(_wsUrl, token: _lastToken!);
- }
+ // 重连前钩子:刷新即将过期的 token 等
+ await onBeforeReconnect?.call();
+ // token 可能被 onBeforeReconnect 更新(通过 updateToken 链路同步)
+ if (_lastToken != null && !_client.isConnected) {
+ _client.connect(_wsUrl, token: _lastToken!);
}
- },
- );
- }
+ }
+ },
+ );
}
// ── 网络状态变化 ──────────────────────────────────────────────────────────
@@ -328,7 +354,7 @@ class SocketManager {
if (_reconnectOnNetworkRestore && _lastToken != null) {
_reconnectOnNetworkRestore = false;
- // 移动端模式:在后台不重连,等前台恢复时再连
+ // 后台断连模式:在后台不重连,等前台恢复时再连
if (_isInBackground && disconnectInBackground) {
_reconnectOnForeground = true;
_log('Network restored but in background, defer to foreground');
@@ -415,7 +441,10 @@ class SocketManager {
/// 发送前置检查
///
- /// 两重保险:连接状态 + 后台状态。
+ /// 后台保活模式(disconnectInBackground=false):只检查连接状态,
+ /// 后台也能正常发送。
+ ///
+ /// 后台断连模式(disconnectInBackground=true):额外检查后台状态,
/// 后台已断连所以 isConnected 通常就能拦住,
/// 但显式检查 _isInBackground 防止边界情况遗漏。
bool _canSend() {
@@ -424,7 +453,7 @@ class SocketManager {
return false;
}
if (_isInBackground && disconnectInBackground) {
- _log('In background, skip send');
+ _log('In background (disconnect mode), skip send');
return false;
}
return true;
diff --git a/apps/im_app/lib/data/local/drift/app_database.dart b/apps/im_app/lib/data/local/drift/app_database.dart
index d854481..935c7e3 100644
--- a/apps/im_app/lib/data/local/drift/app_database.dart
+++ b/apps/im_app/lib/data/local/drift/app_database.dart
@@ -1,27 +1,82 @@
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/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';
-@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 {
-
static Map getTableRegistry(GeneratedDatabase database) {
if (database is! AppDatabase) {
- return {
- };
+ return {};
}
return {
- User: database.users,
- TestTable: database.testTables,
+ DriftFavourite: database.favourites,
+ 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);
+ //升级数据库用此版本号
@override
- int get schemaVersion => 1;
+ int get schemaVersion => 2;
@override
MigrationStrategy get migration {
@@ -30,9 +85,20 @@ class AppDatabase extends _$AppDatabase {
await m.createAll();
},
onUpgrade: (m, from, to) async {
- // 自动检测并添加缺失列
+ // Create any new tables that don't exist yet
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
.customSelect('PRAGMA table_info(${table.actualTableName})')
.get();
@@ -42,7 +108,6 @@ class AppDatabase extends _$AppDatabase {
for (final column in table.$columns) {
if (!existingNames.contains(column.name)) {
- //字段缺失,添加。
await m.addColumn(table, column);
}
}
@@ -50,6 +115,4 @@ class AppDatabase extends _$AppDatabase {
},
);
}
-
-
}
diff --git a/apps/im_app/lib/data/local/drift/tables/call_logs.dart b/apps/im_app/lib/data/local/drift/tables/call_logs.dart
new file mode 100644
index 0000000..4087d94
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/call_logs.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'call_log';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/chat_bots.dart b/apps/im_app/lib/data/local/drift/tables/chat_bots.dart
new file mode 100644
index 0000000..57ef057
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/chat_bots.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'chat_bot';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/chat_categories.dart b/apps/im_app/lib/data/local/drift/tables/chat_categories.dart
new file mode 100644
index 0000000..9e2f7c0
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/chat_categories.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'chat_category';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/chats.dart b/apps/im_app/lib/data/local/drift/tables/chats.dart
new file mode 100644
index 0000000..204bf13
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/chats.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'chat';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/discover_mini_apps.dart b/apps/im_app/lib/data/local/drift/tables/discover_mini_apps.dart
new file mode 100644
index 0000000..6cec9e0
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/discover_mini_apps.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'discover_mini_app';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/explore_mini_apps.dart b/apps/im_app/lib/data/local/drift/tables/explore_mini_apps.dart
new file mode 100644
index 0000000..af55a46
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/explore_mini_apps.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'explore_mini_app';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/favorite_mini_apps.dart b/apps/im_app/lib/data/local/drift/tables/favorite_mini_apps.dart
new file mode 100644
index 0000000..8bb732e
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/favorite_mini_apps.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'favorite_mini_app';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/favourite_details.dart b/apps/im_app/lib/data/local/drift/tables/favourite_details.dart
new file mode 100644
index 0000000..463bb18
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/favourite_details.dart
@@ -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';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/favourites.dart b/apps/im_app/lib/data/local/drift/tables/favourites.dart
new file mode 100644
index 0000000..09ae027
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/favourites.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'favourite';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/groups.dart b/apps/im_app/lib/data/local/drift/tables/groups.dart
new file mode 100644
index 0000000..227a4f3
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/groups.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'chat_group';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/message.dart b/apps/im_app/lib/data/local/drift/tables/message.dart
new file mode 100644
index 0000000..99a2e84
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/message.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'message';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/pending_friend_request_histories.dart b/apps/im_app/lib/data/local/drift/tables/pending_friend_request_histories.dart
new file mode 100644
index 0000000..6864fd4
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/pending_friend_request_histories.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'pending_friend_request_histories';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/recent_mini_apps.dart b/apps/im_app/lib/data/local/drift/tables/recent_mini_apps.dart
new file mode 100644
index 0000000..e3e1cad
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/recent_mini_apps.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'recent_mini_app';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/retries.dart b/apps/im_app/lib/data/local/drift/tables/retries.dart
new file mode 100644
index 0000000..10b0f13
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/retries.dart
@@ -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';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/sounds.dart b/apps/im_app/lib/data/local/drift/tables/sounds.dart
new file mode 100644
index 0000000..45c5a5b
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/sounds.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'sound';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/tags.dart b/apps/im_app/lib/data/local/drift/tables/tags.dart
new file mode 100644
index 0000000..10d143b
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/tags.dart
@@ -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';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/test_tables.dart b/apps/im_app/lib/data/local/drift/tables/test_tables.dart
deleted file mode 100644
index 3d9f9ca..0000000
--- a/apps/im_app/lib/data/local/drift/tables/test_tables.dart
+++ /dev/null
@@ -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';
-}
\ No newline at end of file
diff --git a/apps/im_app/lib/data/local/drift/tables/user_request_histories.dart b/apps/im_app/lib/data/local/drift/tables/user_request_histories.dart
new file mode 100644
index 0000000..ffb15f5
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/user_request_histories.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'user_request_history';
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/users.dart b/apps/im_app/lib/data/local/drift/tables/users.dart
index 57d48e5..88745df 100644
--- a/apps/im_app/lib/data/local/drift/tables/users.dart
+++ b/apps/im_app/lib/data/local/drift/tables/users.dart
@@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
-@DataClassName('User')
+@DataClassName('DriftUser')
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
- IntColumn get uid => integer().nullable()();
+ IntColumn get uid => integer().unique()();
TextColumn get uuid => text().nullable()();
IntColumn get lastOnline => integer().nullable()();
TextColumn get profilePic => text().nullable()();
@@ -28,14 +28,18 @@ class Users extends Table {
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))();
+ 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 => 'user';
-}
\ No newline at end of file
+}
diff --git a/apps/im_app/lib/data/local/drift/tables/workspaces.dart b/apps/im_app/lib/data/local/drift/tables/workspaces.dart
new file mode 100644
index 0000000..8d9fe9a
--- /dev/null
+++ b/apps/im_app/lib/data/local/drift/tables/workspaces.dart
@@ -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 get primaryKey => {id};
+
+ @override
+ String get tableName => 'workspace';
+}
diff --git a/apps/im_app/lib/data/models/call_log_dto.dart b/apps/im_app/lib/data/models/call_log_dto.dart
new file mode 100644
index 0000000..6c065e1
--- /dev/null
+++ b/apps/im_app/lib/data/models/call_log_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/chat_bot_dto.dart b/apps/im_app/lib/data/models/chat_bot_dto.dart
new file mode 100644
index 0000000..4bf7f9d
--- /dev/null
+++ b/apps/im_app/lib/data/models/chat_bot_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/chat_category_dto.dart b/apps/im_app/lib/data/models/chat_category_dto.dart
new file mode 100644
index 0000000..65daf61
--- /dev/null
+++ b/apps/im_app/lib/data/models/chat_category_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/chat_dto.dart b/apps/im_app/lib/data/models/chat_dto.dart
new file mode 100644
index 0000000..5f15487
--- /dev/null
+++ b/apps/im_app/lib/data/models/chat_dto.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/data/models/discover_mini_app_dto.dart b/apps/im_app/lib/data/models/discover_mini_app_dto.dart
new file mode 100644
index 0000000..e558a85
--- /dev/null
+++ b/apps/im_app/lib/data/models/discover_mini_app_dto.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/data/models/explore_mini_app_dto.dart b/apps/im_app/lib/data/models/explore_mini_app_dto.dart
new file mode 100644
index 0000000..20ae5a0
--- /dev/null
+++ b/apps/im_app/lib/data/models/explore_mini_app_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/favorite_mini_app_dto.dart b/apps/im_app/lib/data/models/favorite_mini_app_dto.dart
new file mode 100644
index 0000000..f035765
--- /dev/null
+++ b/apps/im_app/lib/data/models/favorite_mini_app_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/favourite_detail_dto.dart b/apps/im_app/lib/data/models/favourite_detail_dto.dart
new file mode 100644
index 0000000..6300e6c
--- /dev/null
+++ b/apps/im_app/lib/data/models/favourite_detail_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/favourite_dto.dart b/apps/im_app/lib/data/models/favourite_dto.dart
new file mode 100644
index 0000000..434d424
--- /dev/null
+++ b/apps/im_app/lib/data/models/favourite_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/group_dto.dart b/apps/im_app/lib/data/models/group_dto.dart
new file mode 100644
index 0000000..8f14838
--- /dev/null
+++ b/apps/im_app/lib/data/models/group_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/message_dto.dart b/apps/im_app/lib/data/models/message_dto.dart
new file mode 100644
index 0000000..762b4eb
--- /dev/null
+++ b/apps/im_app/lib/data/models/message_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/pending_friend_request_history_dto.dart b/apps/im_app/lib/data/models/pending_friend_request_history_dto.dart
new file mode 100644
index 0000000..209ed45
--- /dev/null
+++ b/apps/im_app/lib/data/models/pending_friend_request_history_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/recent_mini_app_dto.dart b/apps/im_app/lib/data/models/recent_mini_app_dto.dart
new file mode 100644
index 0000000..915f2aa
--- /dev/null
+++ b/apps/im_app/lib/data/models/recent_mini_app_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/retry_dto.dart b/apps/im_app/lib/data/models/retry_dto.dart
new file mode 100644
index 0000000..feb9bd5
--- /dev/null
+++ b/apps/im_app/lib/data/models/retry_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/sound_dto.dart b/apps/im_app/lib/data/models/sound_dto.dart
new file mode 100644
index 0000000..281561f
--- /dev/null
+++ b/apps/im_app/lib/data/models/sound_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/tag_dto.dart b/apps/im_app/lib/data/models/tag_dto.dart
new file mode 100644
index 0000000..32d9910
--- /dev/null
+++ b/apps/im_app/lib/data/models/tag_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/user_dto.dart b/apps/im_app/lib/data/models/user_dto.dart
index 5c05b30..b47ae3c 100644
--- a/apps/im_app/lib/data/models/user_dto.dart
+++ b/apps/im_app/lib/data/models/user_dto.dart
@@ -1,68 +1,148 @@
-import 'package:json_annotation/json_annotation.dart';
-
-import '../../domain/entities/user.dart';
-
-part 'user_dto.g.dart';
+import 'package:drift/drift.dart';
+import 'package:im_app/data/local/drift/app_database.dart';
+import 'package:im_app/domain/entities/user.dart';
/// 用户 DTO(Data Transfer Object)
///
/// local / remote 共用的数据传输对象,放在 data/models/。
/// 提供与 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 {
- @JsonKey(name: 'user_id')
- final String userId;
- final String email;
+ final int uid;
+ final String? uuid;
+ final int? lastOnline;
+ final String? profilePic;
+ final String? profilePicGaussian;
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({
- required this.userId,
- required this.email,
+ required this.uid,
+ this.uuid,
+ this.lastOnline,
+ this.profilePic,
+ this.profilePicGaussian,
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 json) =>
- _$UserDtoFromJson(json);
+ factory UserDto.fromJson(Map json) => UserDto(
+ 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 toJson() => _$UserDtoToJson(this);
+ Map 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
- User toEntity() {
- return User(
- id: userId,
- email: email,
- nickname: nickname,
- avatar: avatar,
- );
- }
+ 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,
+ );
/// Domain Entity → DTO
- factory UserDto.fromEntity(User user) {
- return UserDto(
- userId: user.id,
- email: user.email,
- nickname: user.nickname,
- avatar: user.avatar,
- );
- }
+ factory UserDto.fromEntity(User user) => UserDto(
+ uid: user.uid,
+ uuid: user.uuid,
+ lastOnline: user.lastOnline,
+ profilePic: user.profilePic,
+ profilePicGaussian: user.profilePicGaussian,
+ nickname: user.nickname,
+ 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),
+ );
}
diff --git a/apps/im_app/lib/data/models/user_request_history_dto.dart b/apps/im_app/lib/data/models/user_request_history_dto.dart
new file mode 100644
index 0000000..0749a74
--- /dev/null
+++ b/apps/im_app/lib/data/models/user_request_history_dto.dart
@@ -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 json) =>
+ UserRequestHistoryDto(
+ id: json['id'] as int,
+ status: json['status'],
+ createdAt: json['created_at'],
+ );
+
+ Map 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),
+ );
+}
diff --git a/apps/im_app/lib/data/models/workspace_dto.dart b/apps/im_app/lib/data/models/workspace_dto.dart
new file mode 100644
index 0000000..a404765
--- /dev/null
+++ b/apps/im_app/lib/data/models/workspace_dto.dart
@@ -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 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 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),
+ );
+}
diff --git a/apps/im_app/lib/data/remote/get_profile_request.dart b/apps/im_app/lib/data/remote/get_profile_request.dart
index a02a3bb..c51ed81 100644
--- a/apps/im_app/lib/data/remote/get_profile_request.dart
+++ b/apps/im_app/lib/data/remote/get_profile_request.dart
@@ -28,31 +28,75 @@ part 'get_profile_request.g.dart';
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
@JsonSerializable(createToJson: false)
class ProfileData {
- @JsonKey(name: 'user_id')
- final String userId;
+ final int uid;
+ 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? nickname;
- final String? avatar;
+ @JsonKey(name: 'recovery_email')
+ 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({
- 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,
- this.nickname,
- this.avatar,
+ required this.recoveryEmail,
+ required this.username,
+ required this.bio,
+ required this.relationship,
+ this.userAlias,
+ required this.channelId,
+ required this.channelGroupId,
+ required this.hint,
});
factory ProfileData.fromJson(Map json) =>
_$ProfileDataFromJson(json);
/// DTO → Domain Entity
- User toEntity() {
- return User(
- id: userId,
- email: email,
- nickname: nickname,
- avatar: avatar,
- );
- }
+ 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,
+ );
}
// ─────────────────────────────────────────────
@@ -61,7 +105,7 @@ class ProfileData {
/// 获取用户资料请求(GET,无参数)
///
-/// GET 请求无 body,toJson() 返回空 map。
+/// GET 请求无 body,mixin 自动生成 toJson() → 空 map。
/// 如需 query 参数(如分页),添加字段即可,
/// toJson() 会自动将字段序列化为 URL query string。
@ApiRequest(
@@ -69,14 +113,7 @@ class ProfileData {
method: HttpMethod.get,
responseType: ProfileData,
)
-@JsonSerializable()
class GetProfileRequest extends ApiRequestable
with _$GetProfileRequestApi {
GetProfileRequest();
-
- factory GetProfileRequest.fromJson(Map json) =>
- _$GetProfileRequestFromJson(json);
-
- @override
- Map toJson() => _$GetProfileRequestToJson(this);
}
diff --git a/apps/im_app/lib/data/remote/login_request.dart b/apps/im_app/lib/data/remote/login_request.dart
index e5e6468..9d1c976 100644
--- a/apps/im_app/lib/data/remote/login_request.dart
+++ b/apps/im_app/lib/data/remote/login_request.dart
@@ -8,57 +8,140 @@ part 'login_request.g.dart';
/// # /auth/login — 登录接口
///
-/// 一个端点 = 一个文件,Response DTO + Request 放在同一文件中。
-///
/// ## 数据流位置
///
/// ```
/// AuthRepositoryImpl.login(email, password)
/// → _client.executeRequest( ★ LoginRequest ★ ) ← 你在这里
/// → 服务端 POST /auth/login
-/// → 响应 JSON → ★ LoginData ★ ← 也在这里
-/// → LoginData.toEntity() → User
+/// → 响应 JSON → ★ LoginResponse ★ ← 也在这里
+/// → LoginResponse.toEntity() → User
/// ```
// ─────────────────────────────────────────────
// Response DTO
// ─────────────────────────────────────────────
-/// 登录响应 DTO
-///
-/// 服务端返回的登录数据,包含 token 和用户信息。
-/// 通过 [toEntity] 转换为 Domain Entity [User]。
-@JsonSerializable()
-class LoginData {
- final String token;
- @JsonKey(name: 'user_id')
- final String userId;
+@JsonSerializable(createToJson: false)
+class LoginProfile {
+ final int uid;
+ 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? nickname;
- final String? avatar;
+ @JsonKey(name: 'recovery_email')
+ 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 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({
- required this.token,
- required this.userId,
- required this.email,
- this.nickname,
- this.avatar,
+ required this.accountId,
+ required this.profile,
+ required this.nonce,
+ required this.accessToken,
+ required this.refreshToken,
+ required this.deviceId,
+ required this.loginData,
});
factory LoginData.fromJson(Map json) =>
_$LoginDataFromJson(json);
- Map toJson() => _$LoginDataToJson(this);
+ User toEntity() => profile.toEntity();
+}
- /// DTO → Domain Entity
- User toEntity() {
- return User(
- id: userId,
- email: email,
- nickname: nickname,
- avatar: avatar,
- );
- }
+/// Top-level envelope: { "code": 0, "message": "OK", "data": { ... } }
+@JsonSerializable(createToJson: false, explicitToJson: true)
+class LoginResponse {
+ final int code;
+ final String message;
+ final LoginData data;
+
+ const LoginResponse({
+ required this.code,
+ required this.message,
+ required this.data,
+ });
+
+ factory LoginResponse.fromJson(Map json) =>
+ _$LoginResponseFromJson(json);
+
+ User toEntity() => data.toEntity();
}
// ─────────────────────────────────────────────
@@ -67,24 +150,21 @@ class LoginData {
/// 登录请求
///
-/// `@ApiRequest` 自动生成 `_$LoginRequestApi` mixin,
-/// 提供 path / method / requestType / includeToken / fromJson 自动注册。
+/// `@ApiRequest` 一个注解搞定一切:
+/// - mixin 自动生成 path / method / requestType / includeToken / toJson
+/// - toJson 只序列化类自身字段(email, password),不含继承属性
+/// - Response 的 fromJson 在 parameters getter 中自动注册
+/// - 无需 @JsonSerializable,无需手写 fromJson / toJson
@ApiRequest(
path: ApiPaths.authLogin,
method: HttpMethod.post,
- responseType: LoginData,
+ responseType: LoginResponse,
requestType: ApiRequestType.login,
)
-@JsonSerializable()
-class LoginRequest extends ApiRequestable with _$LoginRequestApi {
+class LoginRequest extends ApiRequestable
+ with _$LoginRequestApi {
final String email;
final String password;
LoginRequest({required this.email, required this.password});
-
- factory LoginRequest.fromJson(Map json) =>
- _$LoginRequestFromJson(json);
-
- @override
- Map toJson() => _$LoginRequestToJson(this);
}
diff --git a/apps/im_app/lib/data/remote/upload_file_request.dart b/apps/im_app/lib/data/remote/upload_file_request.dart
index 479946e..abb4440 100644
--- a/apps/im_app/lib/data/remote/upload_file_request.dart
+++ b/apps/im_app/lib/data/remote/upload_file_request.dart
@@ -52,7 +52,7 @@ class UploadResult {
/// FormData 上传请求
///
-/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 信封。
+/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 格式。
/// 无需 override `decodeResponse`。
@ApiRequest(
path: ApiPaths.uploadFile,
@@ -97,7 +97,7 @@ class S3UploadResponse {
/// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL)
/// - uploadData 为 Uint8List 二进制数据
/// - 自定义 headers(Content-Type: application/octet-stream)
-/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准信封
+/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准响应格式
class S3UploadRequest extends ApiRequestable {
final Uint8List data;
final String presignedURL;
@@ -115,8 +115,8 @@ class S3UploadRequest extends ApiRequestable {
@override
Map? get customHeaders => {
- 'Content-Type': 'application/octet-stream',
- };
+ 'Content-Type': 'application/octet-stream',
+ };
@override
Map toJson() => {};
@@ -125,7 +125,7 @@ class S3UploadRequest extends ApiRequestable {
@override
Object? get uploadData => data;
- /// S3 响应不走标准 { code, message, data } 信封,需要自定义解码
+ /// S3 响应不走标准 { code, message, data } 格式,需要自定义解码
///
/// 可能的响应:
/// - 204 No Content(空 body)→ 成功
diff --git a/apps/im_app/lib/data/repositories/auth_repository_impl.dart b/apps/im_app/lib/data/repositories/auth_repository_impl.dart
index 912e628..0c167cb 100644
--- a/apps/im_app/lib/data/repositories/auth_repository_impl.dart
+++ b/apps/im_app/lib/data/repositories/auth_repository_impl.dart
@@ -27,21 +27,25 @@ class AuthRepositoryImpl implements AuthRepository {
final NetworksSdkApi _client;
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
- Future login({required String email, required String password,}) async
- {
- final LoginData? loginData = await _client.executeRequest(LoginRequest(email: email, password: password),);
+ Future login({required String email, required String password}) async {
+ final LoginResponse? loginResponse = await _client.executeRequest(
+ LoginRequest(email: email, password: password),
+ );
- if (loginData == null) {
- throw Exception('Login failed: empty response'); // TODO: 接入国际化
+ if (loginResponse == null) {
+ throw Exception('Login failed: empty response');
}
- // 回调写入 Token(内存 + 持久化由 Provider 层组合)
- _onTokenUpdate(loginData.token);
+ _onTokenUpdate(loginResponse.data.accessToken);
- return loginData.toEntity(); // DTO → Domain Entity
+ return loginResponse.toEntity();
}
@override
diff --git a/apps/im_app/lib/domain/entities/call_log.dart b/apps/im_app/lib/domain/entities/call_log.dart
new file mode 100644
index 0000000..9df7b8f
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/call_log.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/chat.dart b/apps/im_app/lib/domain/entities/chat.dart
new file mode 100644
index 0000000..5f15487
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/chat.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/chat_bot.dart b/apps/im_app/lib/domain/entities/chat_bot.dart
new file mode 100644
index 0000000..34ca0da
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/chat_bot.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/chat_category.dart b/apps/im_app/lib/domain/entities/chat_category.dart
new file mode 100644
index 0000000..2f459f7
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/chat_category.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/discover_mini_app.dart b/apps/im_app/lib/domain/entities/discover_mini_app.dart
new file mode 100644
index 0000000..e558a85
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/discover_mini_app.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/explore_mini_app.dart b/apps/im_app/lib/domain/entities/explore_mini_app.dart
new file mode 100644
index 0000000..b108649
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/explore_mini_app.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/favorite_mini_app.dart b/apps/im_app/lib/domain/entities/favorite_mini_app.dart
new file mode 100644
index 0000000..6fb0890
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/favorite_mini_app.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/favourite.dart b/apps/im_app/lib/domain/entities/favourite.dart
new file mode 100644
index 0000000..9739eaa
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/favourite.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/favourite_detail.dart b/apps/im_app/lib/domain/entities/favourite_detail.dart
new file mode 100644
index 0000000..9d59cff
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/favourite_detail.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/group.dart b/apps/im_app/lib/domain/entities/group.dart
new file mode 100644
index 0000000..a507278
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/group.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/message.dart b/apps/im_app/lib/domain/entities/message.dart
new file mode 100644
index 0000000..ffb7dfb
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/message.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/pending_friend_request_history.dart b/apps/im_app/lib/domain/entities/pending_friend_request_history.dart
new file mode 100644
index 0000000..84e2517
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/pending_friend_request_history.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/recent_mini_app.dart b/apps/im_app/lib/domain/entities/recent_mini_app.dart
new file mode 100644
index 0000000..367a684
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/recent_mini_app.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/retry.dart b/apps/im_app/lib/domain/entities/retry.dart
new file mode 100644
index 0000000..b481708
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/retry.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/sound.dart b/apps/im_app/lib/domain/entities/sound.dart
new file mode 100644
index 0000000..f0b7310
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/sound.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/tag.dart b/apps/im_app/lib/domain/entities/tag.dart
new file mode 100644
index 0000000..62034b5
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/tag.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/user.dart b/apps/im_app/lib/domain/entities/user.dart
index 509d0ed..a801c1c 100644
--- a/apps/im_app/lib/domain/entities/user.dart
+++ b/apps/im_app/lib/domain/entities/user.dart
@@ -14,15 +14,81 @@
/// → View 渲染
/// ```
class User {
- final String id;
- final String email;
+ final int uid;
+ final String? uuid;
+ final int? lastOnline;
+ final String? profilePic;
+ final String? profilePicGaussian;
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({
- required this.id,
- required this.email,
+ required this.uid,
+ this.uuid,
+ this.lastOnline,
+ this.profilePic,
+ this.profilePicGaussian,
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,
+ );
+ }
}
diff --git a/apps/im_app/lib/domain/entities/user_request_history.dart b/apps/im_app/lib/domain/entities/user_request_history.dart
new file mode 100644
index 0000000..7ec3bae
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/user_request_history.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/domain/entities/workspace.dart b/apps/im_app/lib/domain/entities/workspace.dart
new file mode 100644
index 0000000..933af94
--- /dev/null
+++ b/apps/im_app/lib/domain/entities/workspace.dart
@@ -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,
+ );
+ }
+}
diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
index 14f44d3..1abaa52 100644
--- a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
+++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
@@ -58,17 +58,16 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
return ChatDbTestState(testResults: testResults);
}
- // ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
+ // ── 操作(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
- /// 开始测试
- void startDBTest(BuildContext context) {
- state = state.copyWith(testStarted: true, currentState: '开始测试');
- _testDBInsert();
- }
-
- /// 结束测试
- void stopDBTest(BuildContext context) {
- state = state.copyWith(testStarted: false, currentState: '结束测试');
+ /// 切换测试状态(开始 / 停止)
+ void toggleDBTest() {
+ if (state.testStarted) {
+ state = state.copyWith(testStarted: false, currentState: '结束测试');
+ } else {
+ state = state.copyWith(testStarted: true, currentState: '开始测试');
+ _testDBInsert();
+ }
}
Future _testDBInsert() async {
@@ -84,13 +83,11 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
for (var i = 0; i < count; i += chunkSize) {
final chunk = List.generate(
chunkSize.clamp(0, count - i),
- (j) => UsersCompanion.insert(
- uid: Value(i + j),
- nickname: Value('User ${i + j}'),
- ),
+ (j) =>
+ UsersCompanion.insert(uid: i + j, nickname: Value('User ${i + j}')),
);
- await db.batchInsertOrReplace(chunk);
+ await db.batchInsertOrReplace(chunk);
completed += chunk.length;
// 让出主线程
diff --git a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
index 06ec586..95c1f01 100644
--- a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
+++ b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
@@ -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 '../../../core/ui/components/app_button.dart';
-import '../presentation/chat_view_model.dart';
-/// 聊天页(Demo 按钮)
+/// 数据库性能测试页(Demo)
///
-/// 包含五个演示按钮,覆盖 go_router 的常见导航场景:
-/// - 「切换 Tab」 — go,替换历史,不可返回
-/// - 「有参 push(extra)」 — push + extra(Dart Record),可返回
-/// - 「有参 push(路径参数)」— push + URL 内嵌 id,可返回
-/// - 「无参 push」 — push,可返回
-/// - 「退出登录」 — 守卫自动重定向到 /login
-///
-/// 所有操作通过 [ChatViewModel] 处理,View 不直接调用路由。
-/// 正式开发后替换为会话列表,按钮相关代码一并清除。
+/// 批量插入 10000 条用户记录,验证 Drift 批量写入性能。
+/// 所有操作通过 [ChatDbTestViewModel] 处理,View 只负责渲染。
+/// 正式开发后此页面将被删除。
class ChatDbTestPage extends ConsumerWidget {
const ChatDbTestPage({super.key});
@@ -25,7 +18,7 @@ class ChatDbTestPage extends ConsumerWidget {
final state = ref.watch(chatDbTestViewModelProvider);
return Scaffold(
- appBar: AppBar(title: const Text('测试数据库'), ),
+ appBar: AppBar(title: const Text('测试数据库')),
body: Column(
mainAxisSize: MainAxisSize.max,
spacing: 16,
@@ -38,17 +31,14 @@ class ChatDbTestPage extends ConsumerWidget {
children: [
AppButton.inverse(
label: state.testStarted ? '结束' : '开始',
- onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context),
+ onPressed: () => vm.toggleDBTest(),
),
SizedBox(width: 8),
Expanded(
- child: Text(
- state.currentState,
- textAlign: TextAlign.end,
- ),
- )
+ child: Text(state.currentState, textAlign: TextAlign.end),
+ ),
],
- )
+ ),
),
Expanded(
child: ListView.builder(
@@ -63,7 +53,7 @@ class ChatDbTestPage extends ConsumerWidget {
);
},
),
- )
+ ),
],
),
);
diff --git a/apps/im_app/lib/features/login/presentation/login_view_model.dart b/apps/im_app/lib/features/login/presentation/login_view_model.dart
index ec4e928..89f1adb 100644
--- a/apps/im_app/lib/features/login/presentation/login_view_model.dart
+++ b/apps/im_app/lib/features/login/presentation/login_view_model.dart
@@ -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:im_app/app/di/db_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -55,16 +59,25 @@ class LoginViewModel extends _$LoginViewModel {
/// 仅用于演示路由守卫行为,正式 UI 就绪后删除此方法,改用 [login]。
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
Future demoLogin() async {
- final storageApi = ref.read(storageSdkProvider);
+ try {
+ final storageApi = ref.read(storageSdkProvider);
+ final storageLifeCycle = storageApi as StorageSdkLifecycle;
- ///TODO: StorageSDKLifeCycle 需要只在主项目暴露
- final storageLifeCycle = storageApi as StorageSdkLifecycle;
- ref.read(authNotifierProvider).login();
+ // 读取 mock 数据
+ final raw = await rootBundle.loadString('assets/loginData.json');
+ final json = jsonDecode(raw) as Map;
+ final loginResponse = LoginResponse.fromJson(json);
+ final user = loginResponse.data.toEntity();
- await storageLifeCycle.openDatabase(1234567);
- final rows = await storageApi.rawQuery("PRAGMA table_info('user')");
- for (final row in rows) {
- debugPrint('Schema: ${row.data}');
+ // 先完成 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);
}
}
diff --git a/apps/im_app/pubspec.yaml b/apps/im_app/pubspec.yaml
index b20f848..b0d565e 100644
--- a/apps/im_app/pubspec.yaml
+++ b/apps/im_app/pubspec.yaml
@@ -66,3 +66,5 @@ dev_dependencies:
flutter:
uses-material-design: true
+ assets:
+ - assets/
diff --git a/packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart b/packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart
index cd7c99d..9078178 100644
--- a/packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart
+++ b/packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart
@@ -9,6 +9,7 @@
library;
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/session_key.dart';
export 'src/domain/entities/encrypted_message.dart';
diff --git a/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart b/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart
index 481bad3..cd23a12 100644
--- a/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart
+++ b/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart
@@ -1,4 +1,5 @@
import 'dart:convert';
+import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
@@ -7,30 +8,96 @@ import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart' as encrypt_pkg;
import 'package:pointycastle/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/rsa_key_generator.dart';
+import 'package:pointycastle/macs/hmac.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 {
- // ==================== 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 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 = {};
+
+ /// 清空派生密钥缓存(session key 轮换时调用)
+ void clearDerivedKeyCache() => _derivedKeyCache.clear();
+
+ // ==================== RSA 密钥管理 ====================
+
+ /// 生成 RSA 密钥对(同步,阻塞主线程)
+ ///
+ /// 建议使用 [generateRsaKeyPairAsync] 代替,避免 UI 卡顿。
RsaKeyPairResult generateRsaKeyPair({int keySize = 1024}) {
try {
- // Get secure random
final secureRandom = FortunaRandom();
secureRandom.seed(KeyParameter(_generateSecureRandomBytes(32)));
- // Create RSA key generator
final keyGen = RSAKeyGenerator();
keyGen.init(
ParametersWithRandom(
@@ -39,12 +106,10 @@ class EncryptionFlutterService {
),
);
- // Generate key pair
final keyPair = keyGen.generateKeyPair();
final rsaPublicKey = keyPair.publicKey;
final rsaPrivateKey = keyPair.privateKey;
- // Export to PEM format
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
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 generateRsaKeyPairAsync({int keySize = 1024}) async {
+ return await Isolate.run(
+ () => EncryptionFlutterService().generateRsaKeyPair(keySize: keySize),
+ );
+ }
+
+ /// 编码 RSA 公钥为 PEM 格式
String _encodeRSAPublicKey(RSAPublicKey publicKey) {
- // Build RSAPublicKeyInfo structure
final topSeq = ASN1Sequence();
- // AlgorithmIdentifier: OID 1.2.840.113549.1.1.1 + NULL
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());
topSeq.add(algoSeq);
- // RSAPublicKey: modulus + publicExponent
final keySeq = ASN1Sequence();
keySeq.add(ASN1Integer(publicKey.n!));
keySeq.add(ASN1Integer(publicKey.exponent!));
- // BitString wrapping the key (with 0 unused bits prefix)
final keyBytes = keySeq.encodedBytes;
final keyList = List.from(keyBytes);
- keyList.insert(0, 0); // Add unused bits byte
+ keyList.insert(0, 0);
topSeq.add(ASN1BitString(keyList));
final derBytes = topSeq.encodedBytes;
@@ -84,51 +161,32 @@ class EncryptionFlutterService {
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) {
- // Build RSAPrivateKey structure (PKCS#8 format)
final topSeq = ASN1Sequence();
-
- // Version (0)
topSeq.add(ASN1Integer(BigInt.zero));
-
- // Modulus
topSeq.add(ASN1Integer(privateKey.n!));
-
- // Public Exponent
topSeq.add(ASN1Integer(privateKey.exponent!));
-
- // Private Exponent
topSeq.add(ASN1Integer(privateKey.privateExponent!));
-
- // Prime P
topSeq.add(ASN1Integer(privateKey.p!));
-
- // Prime Q
topSeq.add(ASN1Integer(privateKey.q!));
- // (Optional CRT params omitted for simplicity)
-
final derBytes = topSeq.encodedBytes;
final base64 = base64Encode(derBytes.toList());
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({
required String privateKey,
required String password,
}) {
try {
- // Generate AES key from MD5(password)
final aesKey = _md5Hash(password);
-
- // Generate random IV (16 bytes)
final iv = _generateSecureRandomBytes(16);
- // AES encrypt using encrypt package
final secretKey = encrypt_pkg.Key(aesKey);
final encryptor = encrypt_pkg.Encrypter(
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 encryptedBytes = encrypted.bytes;
- // Combine IV + encrypted data
final combined = Uint8List(iv.length + encryptedBytes.length);
combined.setAll(0, iv);
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({
required String encryptedPrivateKey,
required String password,
}) {
try {
- // Generate AES key from MD5(password)
final aesKey = _md5Hash(password);
-
- // Decode Base64
final combined = base64Decode(encryptedPrivateKey);
-
- // Extract IV and encrypted data
final iv = combined.sublist(0, 16);
final encBytes = combined.sublist(16);
- // AES decrypt
final secretKey = encrypt_pkg.Key(aesKey);
final encryptor = encrypt_pkg.Encrypter(
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}) {
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
final key = base64Encode(keyBytes);
@@ -191,16 +242,14 @@ class EncryptionFlutterService {
return SessionKeyResult(key: key, round: initialRound);
}
- /// Encrypt session key with RSA public key
+ /// 用 RSA 公钥加密会话密钥
String encryptSessionKey({
required String sessionKey,
required String publicKey,
}) {
try {
- // Parse RSA public key
final rsaPublicKey = _parsePublicKey(publicKey);
- // RSA encrypt using PKCS1 padding (like native implementations)
final cipher = PKCS1Encoding(RSAEngine());
cipher.init(true, PublicKeyParameter(rsaPublicKey));
@@ -211,16 +260,14 @@ class EncryptionFlutterService {
}
}
- /// Decrypt session key with RSA private key
+ /// 用 RSA 私钥解密会话密钥
String decryptSessionKey({
required String encryptedSessionKey,
required String privateKey,
}) {
try {
- // Parse RSA private key
final rsaPrivateKey = _parsePrivateKey(privateKey);
- // RSA decrypt using PKCS1 padding (like native implementations)
final cipher = PKCS1Encoding(RSAEngine());
cipher.init(false, PrivateKeyParameter(rsaPrivateKey));
@@ -231,22 +278,18 @@ class EncryptionFlutterService {
}
}
- // ==================== Message Encryption/Decryption ====================
+ // ==================== 消息加密/解密 ====================
- /// Encrypt message (AES-CTR with round-based key derivation)
+ /// 加密消息(AES-CTR,使用 round 派生密钥)
EncryptedMessageResult encryptMessage({
required String plaintext,
required String sessionKey,
required int round,
}) {
try {
- // Derive key for round
final actualKey = _deriveKeyForRound(sessionKey, round);
-
- // Generate random IV (16 bytes for CTR)
final iv = _generateSecureRandomBytes(16);
- // AES-CTR encrypt
final secretKey = encrypt_pkg.Key(actualKey);
final encryptor = encrypt_pkg.Encrypter(
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 encryptedBytes = encrypted.bytes;
- // Combine IV + encrypted data
final combined = Uint8List(iv.length + encryptedBytes.length);
combined.setAll(0, iv);
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({
required String encryptedData,
required String sessionKey,
required int round,
}) {
try {
- // Derive key for round
final actualKey = _deriveKeyForRound(sessionKey, round);
-
- // Decode Base64
final combined = base64Decode(encryptedData);
-
- // Extract IV and encrypted data
final iv = combined.sublist(0, 16);
final encBytes = combined.sublist(16);
- // AES-CTR decrypt
final secretKey = encrypt_pkg.Key(actualKey);
final encryptor = encrypt_pkg.Encrypter(
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) {
_aesSecret = aesSecret;
}
String? _aesSecret;
- /// Decrypt push notification (AES-GCM)
+ /// 解密推送通知(AES-GCM)
String decryptPushNotification({required String encryptedData}) {
try {
final secret = _aesSecret;
@@ -319,17 +355,11 @@ class EncryptionFlutterService {
throw Exception('AES_SECRET not set');
}
- // Convert hex string to bytes
final secretBytes = _hexStringToBytes(secret);
-
- // Decode Base64
final combined = base64Decode(encryptedData);
-
- // Extract IV and encrypted data
final iv = combined.sublist(0, gcmIvLength);
final encBytes = combined.sublist(gcmIvLength);
- // AES-GCM decrypt
final secretKey = encrypt_pkg.Key(secretBytes);
final encryptor = encrypt_pkg.Encrypter(
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) {
- final random = Random.secure();
final bytes = Uint8List(length);
for (var i = 0; i < length; i++) {
- bytes[i] = random.nextInt(256);
+ bytes[i] = _secureRandom.nextInt(256);
}
return bytes;
}
- /// MD5 hash
+ /// MD5 哈希(用于密码派生密钥)
Uint8List _md5Hash(String input) {
final bytes = utf8.encode(input);
- final hash = md5.convert(bytes).bytes as Uint8List;
- return hash;
+ final hash = md5.convert(bytes).bytes;
+ 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) {
- // Base64 decode session key
- final keyBytes = base64Decode(sessionKey);
+ final modeName = kdfMode == KdfMode.md5 ? 'md5' : 'pbkdf2';
+ final cacheKey = '$sessionKey:$targetRound:$modeName';
- // Apply MD5 for the round (simplified version)
- final hash = md5.convert(keyBytes).bytes as Uint8List;
+ // 缓存命中 — 移至末尾以维护 LRU 顺序
+ final cached = _derivedKeyCache.remove(cacheKey);
+ if (cached != null) {
+ _derivedKeyCache[cacheKey] = cached;
+ return cached;
+ }
- return hash;
+ // 计算派生密钥
+ 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;
}
- /// Parse RSA public key from PEM
+ /// 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) {
final base64 = pem
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
@@ -385,7 +469,6 @@ class EncryptionFlutterService {
.trim();
final bytes = base64Decode(base64);
- // Parse ASN.1 DER format
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
@@ -403,7 +486,7 @@ class EncryptionFlutterService {
);
}
- /// Parse RSA private key from PEM
+ /// 解析 RSA 私钥 PEM
RSAPrivateKey _parsePrivateKey(String pem) {
final base64 = pem
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
@@ -412,7 +495,6 @@ class EncryptionFlutterService {
.trim();
final bytes = base64Decode(base64);
- // Parse ASN.1 DER format
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
final keySeq = asn1Parser.nextObject() as ASN1Sequence;
@@ -429,7 +511,7 @@ class EncryptionFlutterService {
);
}
- /// Convert hex string to bytes
+ /// Hex 字符串转字节
Uint8List _hexStringToBytes(String hex) {
final len = hex.length;
final data = Uint8List(len ~/ 2);
diff --git a/packages/cipher_guard_sdk/lib/src/data/repositories/encryption_repository_impl.dart b/packages/cipher_guard_sdk/lib/src/data/repositories/encryption_repository_impl.dart
index 000815e..cd3efd0 100644
--- a/packages/cipher_guard_sdk/lib/src/data/repositories/encryption_repository_impl.dart
+++ b/packages/cipher_guard_sdk/lib/src/data/repositories/encryption_repository_impl.dart
@@ -16,7 +16,8 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
@override
Future generateRsaKeyPair({int keySize = 1024}) async {
- final result = _service.generateRsaKeyPair(keySize: keySize);
+ // 在 Isolate 中运行,避免阻塞主线程(1024-bit 约 150ms)
+ final result = await _service.generateRsaKeyPairAsync(keySize: keySize);
return RsaKeyPair(
publicKey: result.publicKey,
privateKey: result.privateKey,
@@ -50,10 +51,7 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
@override
Future generateSessionKey({int initialRound = 1}) async {
final result = _service.generateSessionKey(initialRound: initialRound);
- return SessionKey(
- key: result.key,
- round: result.round,
- );
+ return SessionKey(key: result.key, round: result.round);
}
@override
@@ -91,10 +89,7 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
sessionKey: sessionKey,
round: round,
);
- return EncryptedMessage(
- round: result.round,
- data: result.data,
- );
+ return EncryptedMessage(round: result.round, data: result.data);
}
@override
@@ -110,6 +105,11 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
);
}
+ // ==================== 缓存管理 ====================
+
+ @override
+ void clearDerivedKeyCache() => _service.clearDerivedKeyCache();
+
// ==================== 原生平台同步 ====================
@override
@@ -147,4 +147,3 @@ class EncryptionRepositoryImpl implements EncryptionRepository {
return _service.decryptPushNotification(encryptedData: encryptedData);
}
}
-
diff --git a/packages/cipher_guard_sdk/lib/src/domain/repositories/encryption_repository.dart b/packages/cipher_guard_sdk/lib/src/domain/repositories/encryption_repository.dart
index c272366..b817d3a 100644
--- a/packages/cipher_guard_sdk/lib/src/domain/repositories/encryption_repository.dart
+++ b/packages/cipher_guard_sdk/lib/src/domain/repositories/encryption_repository.dart
@@ -8,7 +8,7 @@ import '../entities/encrypted_message.dart';
abstract class EncryptionRepository {
// ==================== RSA 金鑰管理 ====================
-
+
/// 生成 RSA 金鑰對
/// [keySize] 金鑰長度 (預設 1024, 可用 2048)
Future generateRsaKeyPair({int keySize = 1024});
@@ -84,6 +84,14 @@ abstract class EncryptionRepository {
required Map> chatMap,
});
+ // ==================== 缓存管理 ====================
+
+ /// 清空派生密钥缓存
+ ///
+ /// 在 session key 轮换时调用,确保旧密钥的派生结果不会被复用。
+ /// 不影响已加密的消息,只影响后续加解密操作的密钥派生。
+ void clearDerivedKeyCache();
+
// ==================== 配置相關 ====================
/// 設置 AES_SECRET (用於推送解密)
@@ -91,8 +99,5 @@ abstract class EncryptionRepository {
/// 解密 APNS 推送通知內容
/// 使用 release.json 中的 AES_SECRET
- Future decryptPushNotification({
- required String encryptedData,
- });
+ Future decryptPushNotification({required String encryptedData});
}
-
diff --git a/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart b/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart
index 584c487..e8a56cb 100644
--- a/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart
+++ b/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart
@@ -7,59 +7,93 @@ 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/presentation/wiring/cipher_guard_sdk_wiring.dart';
-abstract class CipherGuardSdkApi
-{
+abstract class CipherGuardSdkApi {
factory CipherGuardSdkApi() => CipherGuardSdkWiring.build();
// ==================== 平台版本 ====================
-
+
/// 獲取平台版本
Future platformVersion();
// ==================== RSA 金鑰管理 ====================
-
+
/// 生成 RSA 金鑰對
Future generateRsaKeyPair({int keySize = 1024});
/// 用密碼加密私鑰
- Future encryptPrivateKey({required String privateKey, required String password,});
+ Future encryptPrivateKey({
+ required String privateKey,
+ required String password,
+ });
/// 解密私鑰
- Future decryptPrivateKey({required String encryptedPrivateKey, required String password,});
+ Future decryptPrivateKey({
+ required String encryptedPrivateKey,
+ required String password,
+ });
// ==================== 會話金鑰管理 ====================
-
+
/// 生成 AES 會話金鑰
Future generateSessionKey({int initialRound = 1});
/// 用 RSA 公鑰加密會話金鑰
- Future encryptSessionKey({required String sessionKey, required String publicKey,});
+ Future encryptSessionKey({
+ required String sessionKey,
+ required String publicKey,
+ });
/// 用 RSA 私鑰解密會話金鑰
- Future decryptSessionKey({required String encryptedSessionKey, required String privateKey,});
+ Future decryptSessionKey({
+ required String encryptedSessionKey,
+ required String privateKey,
+ });
// ==================== 訊息加解密 ====================
-
+
/// 加密訊息
- Future encryptMessage({required String plaintext, required String sessionKey, required int round,});
+ Future encryptMessage({
+ required String plaintext,
+ required String sessionKey,
+ required int round,
+ });
/// 解密訊息
- Future decryptMessage({required String encryptedData, required String sessionKey, required int round,});
+ Future decryptMessage({
+ required String encryptedData,
+ required String sessionKey,
+ required int round,
+ });
+
+ // ==================== 缓存管理 ====================
+
+ /// 清空派生密钥缓存
+ ///
+ /// session key 轮换后必须调用,否则旧 key 的派生结果可能被复用,
+ /// 导致加解密使用错误的密钥。
+ void clearDerivedKeyCache();
// ==================== 原生平台同步 ====================
-
+
/// 同步加密金鑰到原生平台 (iOS App Group)
- Future syncEncryptionKey({required String chatId, required int activeRound, required int round, required String activeKey, required bool isSingle,});
+ Future syncEncryptionKey({
+ required String chatId,
+ required int activeRound,
+ required int round,
+ required String activeKey,
+ required bool isSingle,
+ });
/// 批量同步所有加密聊天室的金鑰
- Future syncAllEncryptionKeys({required Map> chatMap,});
+ Future syncAllEncryptionKeys({
+ required Map> chatMap,
+ });
// ==================== 推送通知解密 ====================
-
+
/// 設置 AES_SECRET (用於推送解密)
Future setAesSecret({required String aesSecret});
/// 解密 APNS 推送通知內容
- Future decryptPushNotification({required String encryptedData,});
+ Future decryptPushNotification({required String encryptedData});
}
-
diff --git a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart
index 175e7ec..a6b5ada 100644
--- a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart
+++ b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart
@@ -93,6 +93,9 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi {
);
}
+ @override
+ void clearDerivedKeyCache() => _core.encryptionRepo.clearDerivedKeyCache();
+
@override
Future syncEncryptionKey({
required String chatId,
@@ -123,9 +126,9 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi {
}
@override
- Future decryptPushNotification({
- required String encryptedData,
- }) {
- return _core.encryptionRepo.decryptPushNotification(encryptedData: encryptedData);
+ Future decryptPushNotification({required String encryptedData}) {
+ return _core.encryptionRepo.decryptPushNotification(
+ encryptedData: encryptedData,
+ );
}
}
diff --git a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_wiring.dart b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_wiring.dart
index 2b36f4a..7f162a4 100644
--- a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_wiring.dart
+++ b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_wiring.dart
@@ -9,9 +9,18 @@ import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_api_im
/// 使用 Flutter 本地加密服務,無需原生平台處理加密邏輯
class CipherGuardSdkWiring {
/// 構建 SDK 實例
- static CipherGuardSdkApi build() {
+ ///
+ /// [kdfMode] — 密钥派生模式,默认 [KdfMode.md5](兼容模式)
+ /// [pbkdf2Iterations] — PBKDF2 迭代次数(仅 pbkdf2 模式生效,默认 10000)
+ static CipherGuardSdkApi build({
+ KdfMode kdfMode = KdfMode.md5,
+ int pbkdf2Iterations = 10000,
+ }) {
// 1. 創建 Flutter 加密服務
- final flutterService = EncryptionFlutterService();
+ final flutterService = EncryptionFlutterService(
+ kdfMode: kdfMode,
+ pbkdf2Iterations: pbkdf2Iterations,
+ );
// 2. 創建 Repository (使用 Flutter 服務)
final repository = EncryptionRepositoryImpl(flutterService);
@@ -39,4 +48,3 @@ class _CipherGuardPlatformImpl implements CipherGuardPlatform {
return 'Flutter Native'; // 所有加密邏輯現在都在 Flutter 端執行
}
}
-
diff --git a/packages/networks_sdk/lib/src/data/datasources/http/api_client.dart b/packages/networks_sdk/lib/src/data/datasources/http/api_client.dart
index 460934d..2da6614 100644
--- a/packages/networks_sdk/lib/src/data/datasources/http/api_client.dart
+++ b/packages/networks_sdk/lib/src/data/datasources/http/api_client.dart
@@ -9,7 +9,14 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
/// REST API 客户端
/// 基于 Dio,提供请求执行入口
///
-/// 拦截器链顺序:Auth → Encryption → 自定义 → Retry → Logging
+/// 拦截器链顺序(onRequest):Auth → 自定义 → Retry → Logging → Encryption
+///
+/// Dio 的 onResponse / onError 按 **逆序** 执行,因此实际响应处理为:
+/// `Encryption(解密) → Logging → Retry(业务码判断) → 自定义 → Auth`
+///
+/// EncryptionInterceptor 放最后,保证:
+/// - onRequest 最后加密(其他拦截器操作明文)
+/// - onResponse 最先解密(其他拦截器看到明文,业务码判断正常工作)
///
/// 使用方式:
/// ```dart
@@ -31,13 +38,15 @@ class ApiClient {
receiveTimeout: const Duration(seconds: 60),
);
- // 挂载拦截器(顺序:Auth → Encryption → 自定义 → Retry → Logging)
+ // 挂载拦截器
+ // onRequest 顺序:Auth → 自定义 → Retry → Logging → Encryption
+ // onResponse 逆序:Encryption(解密) → Logging → Retry(业务码) → 自定义 → Auth
_dio.interceptors.addAll([
AuthInterceptor(config),
- EncryptionInterceptor(config),
if (additionalInterceptors != null) ...additionalInterceptors,
RetryInterceptor(config: config, dio: _dio),
LoggingInterceptor(onLog: config.onLog),
+ EncryptionInterceptor(config),
]);
}
diff --git a/packages/networks_sdk/lib/src/data/datasources/http/interceptor/encryption_interceptor.dart b/packages/networks_sdk/lib/src/data/datasources/http/interceptor/encryption_interceptor.dart
index 92c977f..72cafd3 100644
--- a/packages/networks_sdk/lib/src/data/datasources/http/interceptor/encryption_interceptor.dart
+++ b/packages/networks_sdk/lib/src/data/datasources/http/interceptor/encryption_interceptor.dart
@@ -3,8 +3,12 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
/// 加密拦截器(预留给 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 时自动跳过,不影响正常请求流程。
/// 后续 cipher_guard_sdk 接入后,App 层在 ApiConfig 中注入
@@ -20,7 +24,23 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
///
/// 加密回调接收原始 path、headers、body,返回 [EncryptedRequest],
/// 拦截器根据非 null 字段覆盖请求。
+///
+/// ## Token 重试与重新加密
+///
+/// 瞬态错误重试(5xx / 超时):复用已加密的请求,不重复加密。
+/// Token 刷新重试:加密 headers 可能包含过期 token 衍生值
+/// (如 X-Token、X-Signature),需要恢复加密前状态并用新 token 重新加密。
+/// RetryInterceptor 通过 `_needsReEncryption` 标记通知本拦截器重新加密。
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;
EncryptionInterceptor(this._config);
@@ -36,7 +56,28 @@ class EncryptionInterceptor extends Interceptor {
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 {
+ // 保存加密前快照,Token 重试时恢复
+ options.extra[_preEncryptSnapshotKey] = {
+ 'path': options.path,
+ 'data': options.data,
+ 'contentType': options.contentType,
+ };
+
// 收集当前 headers(转为 Map)
final currentHeaders = {};
options.headers.forEach((key, value) {
@@ -54,11 +95,17 @@ class EncryptionInterceptor extends Interceptor {
}
if (result.headers != null) {
options.headers.addAll(result.headers!);
+ // 记录加密注入的 header key,Token 重试时移除
+ options.extra[_encryptionAddedHeadersKey] = result.headers!.keys
+ .toList();
}
if (result.contentType != null) {
options.contentType = result.contentType;
}
+ // 标记已加密,防止瞬态重试时重复加密
+ options.extra[_encryptedKey] = true;
+
_config.onLog?.call(
'Request encrypted: ${options.path}',
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?;
+ 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
void onResponse(Response response, ResponseInterceptorHandler handler) async {
final decrypt = _config.onDecryptResponse;
@@ -90,6 +172,14 @@ class EncryptionInterceptor extends Interceptor {
return;
}
+ // 响应已是 Map → 未加密(health check、非加密端点等),跳过解密
+ // 加密模式下响应通常是 String(base64)或 List(bytes)
+ // TODO: 接入加密后,若服务端所有端点都加密,可移除此判断
+ if (response.data is Map) {
+ handler.next(response);
+ return;
+ }
+
try {
final decrypted = await decrypt(response.data as Object);
response.data = decrypted;
diff --git a/packages/networks_sdk/lib/src/data/datasources/http/interceptor/retry_interceptor.dart b/packages/networks_sdk/lib/src/data/datasources/http/interceptor/retry_interceptor.dart
index 3cb5490..b02bbec 100644
--- a/packages/networks_sdk/lib/src/data/datasources/http/interceptor/retry_interceptor.dart
+++ b/packages/networks_sdk/lib/src/data/datasources/http/interceptor/retry_interceptor.dart
@@ -78,7 +78,8 @@ class RetryInterceptor extends Interceptor {
if (code != 0 && config.onBusinessError != null) {
final handled = config.onBusinessError!(code, message, requestPath);
if (handled) {
- // App 层已处理,正常传递响应
+ // App 层已处理 → 标记,让 decodeResponse 跳过二次抛错
+ response.requestOptions.extra['_businessErrorHandled'] = true;
handler.next(response);
return;
}
@@ -115,6 +116,9 @@ class RetryInterceptor extends Interceptor {
options.headers['token'] = newToken;
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
options.extra['_isTokenRetry'] = true;
+ // 通知 EncryptionInterceptor:token 变了,需要用新 token 上下文重新加密
+ // 旧的加密 headers(如 X-Token、X-Signature)可能包含过期 token 信息
+ options.extra['_needsReEncryption'] = true;
final retryResponse = await dio.fetch(options);
handler.resolve(retryResponse);
diff --git a/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart b/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart
index 80490b8..a8c887a 100644
--- a/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart
+++ b/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart
@@ -194,6 +194,9 @@ class NetworksSdkMethodChannelDataSource {
///
/// Dio.download 内部用 FileMode.write(从头覆盖),无法正确续传。
/// 这里手动读流并追加写入文件。
+ ///
+ /// 如果服务端不支持 Range 请求(返回 200 而非 206),
+ /// 自动回退为覆盖写入,防止文件损坏。
Future _downloadWithResume({
required String url,
required String savePath,
@@ -215,14 +218,33 @@ class NetworksSdkMethodChannelDataSource {
final stream = response.data?.stream;
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 =
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 raf = file.openSync(mode: FileMode.writeOnlyAppend);
- int received = startBytes;
+ // 不支持续传时用 write 覆盖,支持时用 append 追加
+ final fileMode = isPartialContent
+ ? FileMode.writeOnlyAppend
+ : FileMode.writeOnly;
+ final raf = file.openSync(mode: fileMode);
+ int received = effectiveStartBytes;
try {
await for (final chunk in stream) {
diff --git a/packages/networks_sdk/lib/src/data/datasources/socket/socket_client.dart b/packages/networks_sdk/lib/src/data/datasources/socket/socket_client.dart
index a84b856..031c033 100644
--- a/packages/networks_sdk/lib/src/data/datasources/socket/socket_client.dart
+++ b/packages/networks_sdk/lib/src/data/datasources/socket/socket_client.dart
@@ -16,7 +16,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
/// - Stream 输出(JSON 消息、原始字符串、二进制、连接状态、错误)
/// - 生命周期感知(前后台切换)
/// - Token 热更新(不断连)
-/// - 消息加密/解密钩子(预留给 cipher_guard_sdk)
+/// - 消息加密/解密钩子(预留给 cipher_guard_sdk,ping/pong 走明文不加密)
///
/// ## 使用方式
///
@@ -60,6 +60,15 @@ class SocketClient {
Timer? _reconnectTimer;
final _random = Random();
+ // ── 消息处理 ──
+
+ /// 异步消息处理链,保证解密场景下消息按到达顺序处理
+ ///
+ /// 无解密回调时不使用(同步处理,天然有序)。
+ /// 有解密回调时,每条消息的处理链在前一条之后执行,
+ /// 即使解密耗时不同也不会乱序。
+ Future? _messageProcessingChain;
+
// ── Stream Controllers ──
final _messageController = StreamController