业务更新User所需(企业成员、聊天室群组成员)
This commit is contained in:
24
apps/im_app/lib/domain/entities/company_member.dart
Normal file
24
apps/im_app/lib/domain/entities/company_member.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
|
||||
/// 企业成员的部分 [User] 表示,数据来源于企业成员接口。
|
||||
///
|
||||
/// 企业 API 返回的用户信息极为精简,仅包含身份、显示名称和在线状态。
|
||||
/// 如需完整用户信息,请通过用户详情接口获取完整的 [User]。
|
||||
///
|
||||
/// 注意企业 API 的字段名与标准用户接口不同:
|
||||
/// - `user_id` → [uid] (非 `uid`)
|
||||
/// - `name` → [nickname] (非 `nickname`)
|
||||
class CompanyMember extends User {
|
||||
const CompanyMember({
|
||||
required super.uid,
|
||||
super.nickname,
|
||||
super.lastOnline,
|
||||
super.requireUpsert = true,
|
||||
});
|
||||
|
||||
factory CompanyMember.fromJson(Map<String, dynamic> json) => CompanyMember(
|
||||
uid: json['user_id'] as int,
|
||||
nickname: json['name'],
|
||||
lastOnline: json['last_online'],
|
||||
);
|
||||
}
|
||||
66
apps/im_app/lib/domain/entities/group_member.dart
Normal file
66
apps/im_app/lib/domain/entities/group_member.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
|
||||
/// 群组成员的部分 [User] 表示,数据来源于群组成员接口。
|
||||
///
|
||||
/// 群组 API 返回的用户信息不完整,仅保证身份和在线状态字段有效。
|
||||
/// 构造函数中未列出的 [User] 字段均为 null,不应依赖这些字段。
|
||||
///
|
||||
/// ## 何时使用 [GroupMember] vs [User]
|
||||
/// - 渲染群组成员列表、成员头像等场景使用 [GroupMember]
|
||||
/// - 需要联系方式、音效设置、好友关系等完整信息时,
|
||||
/// 请通过用户详情接口获取完整的 [User]
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// 群组 API 响应
|
||||
/// → GroupMember.fromJson() ← 群组专用字段(user_id、icon…)
|
||||
/// → ★ GroupMember ★
|
||||
/// → 仍是合法的 [User],可在任何需要 [User] 的地方使用
|
||||
/// ```
|
||||
class GroupMember extends User {
|
||||
const GroupMember({
|
||||
required super.uid,
|
||||
super.nickname,
|
||||
super.profilePic,
|
||||
super.profilePicGaussian,
|
||||
super.lastOnline,
|
||||
super.deletedAt,
|
||||
super.role,
|
||||
super.requireUpsert = true,
|
||||
});
|
||||
|
||||
/// 从群组成员 JSON 数据创建 [GroupMember]。
|
||||
///
|
||||
/// 注意群组 API 使用了与用户接口不同的字段名:
|
||||
/// - `user_id` → [uid] (非 `uid`)
|
||||
/// - `user_name` → [nickname] (非 `nickname`)
|
||||
/// - `icon` → [profilePic] (非 `profile_pic`)
|
||||
/// - `icon_gaussian` → [profilePicGaussian]
|
||||
/// - `delete_time` → [deletedAt] (非 `deleted_at`)
|
||||
factory GroupMember.fromJson(Map<String, dynamic> json) => GroupMember(
|
||||
uid: json['user_id'] as int,
|
||||
nickname: json['user_name'],
|
||||
profilePic: json['icon'],
|
||||
profilePicGaussian: json['icon_gaussian'],
|
||||
lastOnline: json['last_online'],
|
||||
deletedAt: json['delete_time'],
|
||||
role: json['role'],
|
||||
);
|
||||
|
||||
/// 序列化为群组成员 JSON 格式。
|
||||
///
|
||||
/// 字段名与 [fromJson] 保持对称。
|
||||
/// `id`/`user_id`、`nickname`/`user_name`、`icon`/`profile_pic`
|
||||
/// 同时输出两种命名,兼容不同约定的下游消费方。
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': uid,
|
||||
'user_id': uid,
|
||||
'user_name': nickname,
|
||||
'nickname': nickname, // 兼容旧命名
|
||||
'icon': profilePic,
|
||||
'profile_pic': profilePic, // 兼容旧命名
|
||||
'icon_gaussian': profilePicGaussian,
|
||||
'last_online': lastOnline,
|
||||
'delete_time': deletedAt,
|
||||
};
|
||||
}
|
||||
@@ -47,6 +47,24 @@ class User {
|
||||
final int? configBits;
|
||||
final String? hint;
|
||||
|
||||
/// 标记此用户数据是否为部分数据,需要 upsert 而非全字段覆盖。
|
||||
///
|
||||
/// 由响应解析层设置,Repository 据此决定写入策略:
|
||||
/// - true → [UserRepository.upsertUser],仅更新非 null 字段,保留本地已有数据
|
||||
/// - false → [UserRepository.insertOrReplaceUser],全字段覆盖(默认)
|
||||
///
|
||||
/// 注意:此字段仅用于内存传递,不会被持久化到 DB。
|
||||
final bool requireUpsert;
|
||||
|
||||
/// TODO(contacts): 添加 localName / localPhoneNumbers,关联本地通讯录。
|
||||
/// 这两个字段是设备侧数据,不应持久化到服务端 payload。
|
||||
///
|
||||
/// TODO(history): status 和 created_at 仅出现在历史用户记录中(如审计日志)。
|
||||
/// 建议用独立的 UserHistory 实体承载,避免污染此 Domain 模型。
|
||||
///
|
||||
/// TODO(online): objectMgr.onlineMgr.updateOnlineTime() 原先在 fromJson() 内
|
||||
/// 作为副作用调用。不要在此处复现——副作用应在 Repository 或 UseCase 层处理。
|
||||
|
||||
const User({
|
||||
required this.uid,
|
||||
this.uuid,
|
||||
@@ -81,9 +99,13 @@ class User {
|
||||
this.publicKey,
|
||||
this.configBits,
|
||||
this.hint,
|
||||
this.requireUpsert = false,
|
||||
});
|
||||
|
||||
/// 直接从网络 JSON 创建 Domain 实体
|
||||
/// 直接从网络 JSON 创建 Domain 实体。
|
||||
///
|
||||
/// [requireUpsert] 默认 false,如响应解析层判断为部分数据,
|
||||
/// 可在调用后通过 copyWith(requireUpsert: true) 标记。
|
||||
factory User.fromJson(Map<String, dynamic> json) => User(
|
||||
uid: json['uid'] as int,
|
||||
uuid: json['uuid'],
|
||||
@@ -120,7 +142,10 @@ class User {
|
||||
hint: json['hint'],
|
||||
);
|
||||
|
||||
/// 仅更新部分字段
|
||||
/// 仅更新部分字段,其余保持不变。
|
||||
///
|
||||
/// 注意:[requireUpsert] 不会随其他字段自动继承,
|
||||
/// 需要显式传入以避免意外的写入策略变更。
|
||||
User copyWith({
|
||||
int? uid,
|
||||
String? uuid,
|
||||
@@ -155,6 +180,7 @@ class User {
|
||||
String? publicKey,
|
||||
int? configBits,
|
||||
String? hint,
|
||||
bool? requireUpsert,
|
||||
}) {
|
||||
return User(
|
||||
uid: uid ?? this.uid,
|
||||
@@ -191,6 +217,7 @@ class User {
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
configBits: configBits ?? this.configBits,
|
||||
hint: hint ?? this.hint,
|
||||
requireUpsert: requireUpsert ?? this.requireUpsert,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
56
apps/im_app/lib/domain/repositories/call_log_repository.dart
Normal file
56
apps/im_app/lib/domain/repositories/call_log_repository.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:im_app/domain/entities/call_log.dart';
|
||||
|
||||
/// 通话记录仓储接口
|
||||
///
|
||||
/// ## 职责
|
||||
/// - StorageSdkApi ↔ Domain CallLog 映射
|
||||
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// 写入:Domain CallLog → _toCompanion() → StorageSdkApi → DB
|
||||
/// 读取:DB row (DriftCallLog) → _toEntity() → Domain CallLog
|
||||
/// 监听:DB 变化 → stream → Domain CallLog → UI
|
||||
/// ```
|
||||
abstract class CallLogRepository {
|
||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 监听所有通话记录,DB 变化自动反映
|
||||
Stream<List<CallLog>> watchAllCallLogs();
|
||||
|
||||
/// 监听指定通话记录
|
||||
Stream<CallLog?> watchCallLog(String id);
|
||||
|
||||
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 读取所有通话记录,按 updated_at 降序
|
||||
Future<List<CallLog>> getCallLogs();
|
||||
|
||||
/// 读取指定通话记录,不存在返回 null
|
||||
Future<CallLog?> getCallLog(String id);
|
||||
|
||||
/// 检查通话记录是否存在
|
||||
Future<bool> isExist(String id);
|
||||
|
||||
/// 获取未读通话数量
|
||||
///
|
||||
/// 统计 is_read = 0 且非主叫的未接/取消/结束/超时通话
|
||||
Future<int> getUnreadCount(int currentUid);
|
||||
|
||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 插入或替换通话记录
|
||||
Future<void> insertOrReplaceCallLog(CallLog callLog);
|
||||
|
||||
/// 批量插入或替换通话记录
|
||||
Future<void> insertOrReplaceCallLogs(List<CallLog> callLogs);
|
||||
|
||||
/// 将所有通话记录标记为已读
|
||||
Future<void> markAllAsRead();
|
||||
|
||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 删除指定通话记录
|
||||
Future<void> deleteCallLog(String id);
|
||||
}
|
||||
53
apps/im_app/lib/domain/repositories/chat_bot_repository.dart
Normal file
53
apps/im_app/lib/domain/repositories/chat_bot_repository.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:im_app/domain/entities/chat_bot.dart';
|
||||
|
||||
/// 聊天机器人仓储接口
|
||||
///
|
||||
/// ## 职责
|
||||
/// - StorageSdkApi ↔ Domain ChatBot 映射
|
||||
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// 写入:Domain ChatBot → _toCompanion() → StorageSdkApi → DB
|
||||
/// 读取:DB row (DriftChatBot) → _toEntity() → Domain ChatBot
|
||||
/// 监听:DB 变化 → stream → Domain ChatBot → UI
|
||||
/// ```
|
||||
abstract class ChatBotRepository {
|
||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 监听所有聊天机器人,DB 变化自动反映
|
||||
Stream<List<ChatBot>> watchAllChatBots();
|
||||
|
||||
/// 监听指定聊天机器人
|
||||
Stream<ChatBot?> watchChatBot(int id);
|
||||
|
||||
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 读取所有聊天机器人
|
||||
///
|
||||
/// [limit] 可选限制数量
|
||||
Future<List<ChatBot>> getChatBots({int? limit});
|
||||
|
||||
/// 读取指定聊天机器人,不存在返回 null
|
||||
Future<ChatBot?> getChatBot(int id);
|
||||
|
||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 插入或替换聊天机器人
|
||||
Future<void> insertOrReplaceChatBot(ChatBot chatBot);
|
||||
|
||||
/// 批量插入或替换聊天机器人
|
||||
Future<void> insertOrReplaceChatBots(List<ChatBot> chatBots);
|
||||
|
||||
/// 更新聊天机器人
|
||||
Future<void> updateChatBot(ChatBot chatBot);
|
||||
|
||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 删除指定聊天机器人
|
||||
Future<void> deleteChatBot(int id);
|
||||
|
||||
/// 清空所有聊天机器人
|
||||
Future<void> clearChatBots();
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:im_app/data/local/drift/app_database.dart';
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
|
||||
/// 用户仓储接口
|
||||
@@ -7,6 +6,7 @@ import 'package:im_app/domain/entities/user.dart';
|
||||
/// - StorageSdkApi ↔ Domain User 映射
|
||||
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||
/// - 实时监听(单个 / 多个 / 全部)
|
||||
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
@@ -53,23 +53,34 @@ abstract class UserRepository {
|
||||
|
||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 保存完整用户(insert or replace)
|
||||
/// 调用方决定是否持久化
|
||||
Future<void> saveUser(User user);
|
||||
/// 插入或替换单个用户
|
||||
Future<void> insertOrReplaceUser(User user);
|
||||
|
||||
/// 批量保存用户(insert or replace)
|
||||
Future<void> saveUsers(List<User> users);
|
||||
/// 批量插入或替换用户
|
||||
Future<void> insertOrReplaceUsers(List<User> users);
|
||||
|
||||
/// 仅更新指定字段,不影响其他列
|
||||
/// 更新单个用户所有字段,按 uid 匹配
|
||||
///
|
||||
/// 示例:
|
||||
/// ```dart
|
||||
/// await repo.updateFields(uid, UsersCompanion(
|
||||
/// nickname: Value('New Name'),
|
||||
/// lastOnline: Value(DateTime.now().millisecondsSinceEpoch),
|
||||
/// ));
|
||||
/// await repo.updateUser(user.copyWith(nickname: 'New Name'));
|
||||
/// ```
|
||||
Future<void> updateFields(int uid, UsersCompanion companion);
|
||||
Future<void> updateUser(User user);
|
||||
|
||||
/// 批量更新用户,每条按 uid 匹配更新所有字段
|
||||
///
|
||||
/// 示例:
|
||||
/// ```dart
|
||||
/// final updated = users.map((u) => u.copyWith(nickname: 'new')).toList();
|
||||
/// await repo.updateUsersBatch(updated);
|
||||
/// ```
|
||||
Future<void> updateUsersBatch(List<User> users);
|
||||
|
||||
/// 单条 upsert
|
||||
Future<void> upsertUser(User user);
|
||||
|
||||
/// 批量 upsert,走单次事务,优于循环调用 [upsertUser]
|
||||
Future<void> upsertUsers(List<User> users);
|
||||
|
||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
import 'package:im_app/domain/repositories/user_repository.dart';
|
||||
|
||||
/// 批量插入用户用例
|
||||
///
|
||||
/// ## 职责
|
||||
/// - 封装用户插入的业务规则
|
||||
/// - 去重(uid 相同时保留最后一个)
|
||||
/// - 分批插入,避免单次写入过大
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// ViewModel
|
||||
/// → InsertUsersUseCase.execute(users)
|
||||
/// → 去重
|
||||
/// → UserRepository.saveUsers(chunk) ← 分批写入
|
||||
/// → onProgress(completed, total) ← 可选进度回调
|
||||
/// ← 实际插入数量
|
||||
/// ```
|
||||
class InsertUsersUseCase {
|
||||
final UserRepository _repo;
|
||||
static const _chunkSize = 200;
|
||||
|
||||
InsertUsersUseCase({required UserRepository userRepository})
|
||||
: _repo = userRepository;
|
||||
|
||||
/// 批量插入用户
|
||||
///
|
||||
/// [users] 要插入的用户列表
|
||||
/// [onProgress] 可选回调,每批完成后触发
|
||||
///
|
||||
/// 返回实际插入数量
|
||||
Future<int> execute(
|
||||
List<User> users, {
|
||||
void Function(int completed, int total, List<User> chunk)? onProgress,
|
||||
}) async {
|
||||
if (users.isEmpty) return 0;
|
||||
|
||||
final deduped = {for (final u in users) u.uid: u}.values.toList();
|
||||
final total = deduped.length;
|
||||
int completed = 0;
|
||||
|
||||
while (completed < total) {
|
||||
final end = (completed + _chunkSize).clamp(0, total);
|
||||
final chunk = deduped.sublist(completed, end);
|
||||
|
||||
await _repo.saveUsers(chunk);
|
||||
completed += chunk.length;
|
||||
|
||||
onProgress?.call(completed, total, chunk);
|
||||
}
|
||||
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user