From 8f77a14818b76ab99526730dd06c8eb21de2b654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Happi=20=28=E5=93=88=E6=AF=94=29?= Date: Mon, 9 Mar 2026 19:15:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=9B=B4=E6=96=B0User?= =?UTF-8?q?=E6=89=80=E9=9C=80=EF=BC=88=E4=BC=81=E4=B8=9A=E6=88=90=E5=91=98?= =?UTF-8?q?=E3=80=81=E8=81=8A=E5=A4=A9=E5=AE=A4=E7=BE=A4=E7=BB=84=E6=88=90?= =?UTF-8?q?=E5=91=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/im_app/lib/app/di/user_provider.dart | 18 +- .../lib/app/notifiers/user_notifier.dart | 44 +++-- apps/im_app/lib/data/models/call_log_dto.dart | 122 ------------ .../call_log_repository_impl.dart | 165 ++++++++++++++++ .../chat_bot_repository_impl.dart | 179 ++++++++++++++++++ .../repositories/user_repository_impl.dart | 177 ++++++++++++++--- .../lib/domain/entities/company_member.dart | 24 +++ .../lib/domain/entities/group_member.dart | 66 +++++++ apps/im_app/lib/domain/entities/user.dart | 31 ++- .../repositories/call_log_repository.dart | 56 ++++++ .../repositories/chat_bot_repository.dart | 53 ++++++ .../domain/repositories/user_repository.dart | 35 ++-- .../usecases/insert_users_use_case.dart | 55 ------ .../chat/call/di/call_log_provider.dart | 29 +++ .../features/chat/di/chat_bot_provider.dart | 29 +++ .../presentation/chat_db_test_view_model.dart | 65 ++++++- .../lib/features/chat/usecases/.gitkeep | 0 .../chat/usecases/delete_users_use_case.dart | 39 ++++ .../chat/usecases/insert_users_use_case.dart | 2 +- .../chat/usecases/update_users_use_case.dart | 53 ++++++ .../features/chat/view/chat_db_test_page.dart | 24 ++- .../login/presentation/login_view_model.dart | 2 +- 22 files changed, 1030 insertions(+), 238 deletions(-) delete mode 100644 apps/im_app/lib/data/models/call_log_dto.dart create mode 100644 apps/im_app/lib/data/repositories/call_log_repository_impl.dart create mode 100644 apps/im_app/lib/data/repositories/chat_bot_repository_impl.dart create mode 100644 apps/im_app/lib/domain/entities/company_member.dart create mode 100644 apps/im_app/lib/domain/entities/group_member.dart create mode 100644 apps/im_app/lib/domain/repositories/call_log_repository.dart create mode 100644 apps/im_app/lib/domain/repositories/chat_bot_repository.dart delete mode 100644 apps/im_app/lib/domain/usecases/insert_users_use_case.dart create mode 100644 apps/im_app/lib/features/chat/call/di/call_log_provider.dart create mode 100644 apps/im_app/lib/features/chat/di/chat_bot_provider.dart delete mode 100644 apps/im_app/lib/features/chat/usecases/.gitkeep create mode 100644 apps/im_app/lib/features/chat/usecases/delete_users_use_case.dart create mode 100644 apps/im_app/lib/features/chat/usecases/update_users_use_case.dart diff --git a/apps/im_app/lib/app/di/user_provider.dart b/apps/im_app/lib/app/di/user_provider.dart index 9d6b95c..a50178e 100644 --- a/apps/im_app/lib/app/di/user_provider.dart +++ b/apps/im_app/lib/app/di/user_provider.dart @@ -1,5 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:im_app/domain/usecases/insert_users_use_case.dart'; +import 'package:im_app/features/chat/usecases/delete_users_use_case.dart'; +import 'package:im_app/features/chat/usecases/insert_users_use_case.dart'; +import 'package:im_app/features/chat/usecases/update_users_use_case.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:im_app/app/di/db_provider.dart'; import 'package:im_app/data/repositories/user_repository_impl.dart'; @@ -24,6 +26,20 @@ final insertUsersUseCaseProvider = Provider((ref) { return InsertUsersUseCase(userRepository: ref.read(userRepositoryProvider)); }); +/// 批量更新用户用例 Provider +/// +/// 取前10条,随机生成昵称,批量更新到 DB +final updateUsersUseCaseProvider = Provider((ref) { + return UpdateUsersUseCase(userRepository: ref.read(userRepositoryProvider)); +}); + +/// 删除前10个用户用例 Provider +/// +/// 取前10条,按 uid 逐条删除 +final deleteUsersUseCaseProvider = Provider((ref) { + return DeleteUsersUseCase(userRepository: ref.read(userRepositoryProvider)); +}); + // ── Streams ─────────────────────────────────────────────────────────────────── @riverpod diff --git a/apps/im_app/lib/app/notifiers/user_notifier.dart b/apps/im_app/lib/app/notifiers/user_notifier.dart index 56ce957..103092e 100644 --- a/apps/im_app/lib/app/notifiers/user_notifier.dart +++ b/apps/im_app/lib/app/notifiers/user_notifier.dart @@ -1,6 +1,5 @@ import 'package:im_app/app/di/user_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:im_app/data/local/drift/app_database.dart'; import 'package:im_app/domain/entities/user.dart'; import 'package:im_app/domain/repositories/user_repository.dart'; @@ -16,10 +15,16 @@ part 'user_notifier.g.dart'; /// // 即时读取,无需 await /// final user = ref.read(userNotifierProvider(123).notifier).current; /// -/// // 部分更新 -/// ref.read(userNotifierProvider(123).notifier).updateFields( -/// UsersCompanion(nickname: Value('New Name')), +/// // 插入或替换 +/// ref.read(userNotifierProvider(123).notifier).insertOrReplaceUser(user); +/// +/// // 单个更新 +/// ref.read(userNotifierProvider(123).notifier).updateUser( +/// user.copyWith(nickname: 'New Name'), /// ); +/// +/// // 批量更新 +/// ref.read(userNotifierProvider(123).notifier).updateUsers(updatedList); /// ``` @riverpod class UserNotifier extends _$UserNotifier { @@ -31,13 +36,11 @@ class UserNotifier extends _$UserNotifier { Future build(int uid) async { ref.onDispose(() => _cached = null); - // Stream starts automatically — uid is the family arg _repo.watchUser(uid).listen((user) { _cached = user; state = AsyncData(user); }); - // Return initial DB value return _repo.getUser(uid); } @@ -47,16 +50,31 @@ class UserNotifier extends _$UserNotifier { // ── 写入 ───────────────────────────────────────────────────────────────── - Future saveUser(User user) async { - await _repo.saveUser(user); - } - - Future updateFields(UsersCompanion companion) async { - await _repo.updateFields(uid, companion); // uid from build arg + /// 插入或替换单个用户 + Future insertOrReplaceUser(User user) async { + await _repo.insertOrReplaceUser(user); } + /// 更新单个用户所有字段,按 uid 匹配 + /// + /// 示例: + /// ```dart + /// await notifier.updateUser(user.copyWith(nickname: 'New Name')); + /// ``` Future updateUser(User user) async { - await _repo.saveUser(user); + await _repo.updateUser(user); + } + + /// 批量更新用户,每条按 uid 匹配更新所有字段 + /// + /// 示例: + /// ```dart + /// await notifier.updateUsers( + /// users.map((u) => u.copyWith(nickname: 'new')).toList(), + /// ); + /// ``` + Future updateUsers(List users) async { + await _repo.updateUsersBatch(users); } // ── 删除 ───────────────────────────────────────────────────────────────── diff --git a/apps/im_app/lib/data/models/call_log_dto.dart b/apps/im_app/lib/data/models/call_log_dto.dart deleted file mode 100644 index 6c065e1..0000000 --- a/apps/im_app/lib/data/models/call_log_dto.dart +++ /dev/null @@ -1,122 +0,0 @@ -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/repositories/call_log_repository_impl.dart b/apps/im_app/lib/data/repositories/call_log_repository_impl.dart new file mode 100644 index 0000000..02ae552 --- /dev/null +++ b/apps/im_app/lib/data/repositories/call_log_repository_impl.dart @@ -0,0 +1,165 @@ +import 'package:drift/drift.dart'; +import 'package:im_app/data/local/drift/app_database.dart'; +import 'package:im_app/domain/entities/call_log.dart'; +import 'package:im_app/domain/repositories/call_log_repository.dart'; +import 'package:storage_sdk/storage_sdk.dart'; + +/// 通话记录仓储实现 +/// +/// ## 职责 +/// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase +/// - DriftCallLog ↔ Domain CallLog 映射 +/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在此类 +/// +/// ## 数据流 +/// ``` +/// 网络:CallLog.fromJson(json) → insertOrReplaceCallLog(callLog) → StorageSdkApi → DB +/// 监听:StorageSdkApi.watchAll → DriftCallLog → _toEntity() → UI +/// ``` +class CallLogRepositoryImpl implements CallLogRepository { + final StorageSdkApi _storage; + + CallLogRepositoryImpl(this._storage); + + // ── DB row → Domain ────────────────────────────────────────────────────── + + CallLog _toEntity(DriftCallLog row) => CallLog( + id: row.id, + callerId: row.callerId, + receiverId: row.receiverId, + chatId: row.chatId, + duration: row.duration, + videoCall: row.videoCall, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + endedAt: row.endedAt, + status: row.status, + isDeleted: row.isDeleted, + deletedAt: row.deletedAt, + isRead: row.isRead, + ); + + // ── Domain → DB companion (internal only) ──────────────────────────────── + + CallLogsCompanion _toCompanion(CallLog callLog) => CallLogsCompanion( + id: Value(callLog.id), + callerId: Value(callLog.callerId), + receiverId: Value(callLog.receiverId), + chatId: Value(callLog.chatId), + duration: Value(callLog.duration), + videoCall: Value(callLog.videoCall), + createdAt: Value(callLog.createdAt), + updatedAt: Value(callLog.updatedAt), + endedAt: Value(callLog.endedAt), + status: Value(callLog.status), + isDeleted: Value(callLog.isDeleted), + deletedAt: Value(callLog.deletedAt), + isRead: Value(callLog.isRead), + ); + + // ── 监听 ───────────────────────────────────────────────────────────────── + + @override + Stream> watchAllCallLogs() { + return _storage.watchAll().map( + (rows) => rows.map(_toEntity).toList(), + ); + } + + @override + Stream watchCallLog(String id) { + return _storage + .watchFirst((t) => t.id.equals(id)) + .map((row) => row != null ? _toEntity(row) : null); + } + + // ── 读取 ───────────────────────────────────────────────────────────────── + + @override + Future> getCallLogs() async { + final rows = await _storage.rawQuery( + 'SELECT * FROM call_log ORDER BY updated_at DESC', + [], + ); + return rows + .map( + (row) => CallLog( + id: row.read('id'), + callerId: row.readNullable('caller_id'), + receiverId: row.readNullable('receiver_id'), + chatId: row.readNullable('chat_id'), + duration: row.readNullable('duration'), + videoCall: row.readNullable('video_call'), + createdAt: row.readNullable('created_at'), + updatedAt: row.readNullable('updated_at'), + endedAt: row.readNullable('ended_at'), + status: row.readNullable('status'), + isDeleted: row.readNullable('is_deleted'), + deletedAt: row.readNullable('deleted_at'), + isRead: row.readNullable('is_read'), + ), + ) + .toList(); + } + + @override + Future getCallLog(String id) async { + final row = await _storage.selectFirst( + (t) => t.id.equals(id), + ); + return row != null ? _toEntity(row) : null; + } + + @override + Future isExist(String id) async { + final row = await _storage.selectFirst( + (t) => t.id.equals(id), + ); + return row != null; + } + + @override + Future getUnreadCount(int currentUid) async { + final result = await _storage.rawQuery( + ''' + SELECT COUNT(*) as count FROM call_log + WHERE is_read = 0 + AND caller_id != ? + AND (status = 3 OR status = 4 OR status = 5 OR status = 6) + ''', + [currentUid], + ); + if (result.isNotEmpty) { + return result.first.read('count'); + } + return 0; + } + + // ── 写入 ───────────────────────────────────────────────────────────────── + + @override + Future insertOrReplaceCallLog(CallLog callLog) async { + await _storage.insertOrReplace(_toCompanion(callLog)); + } + + @override + Future insertOrReplaceCallLogs(List callLogs) async { + await _storage.batchInsertOrReplace( + callLogs.map(_toCompanion).toList(), + ); + } + + @override + Future markAllAsRead() async { + await _storage.rawQuery('UPDATE call_log SET is_read = 1', []); + } + + // ── 删除 ───────────────────────────────────────────────────────────────── + + @override + Future deleteCallLog(String id) async { + await _storage.deleteWhere( + (t) => t.id.equals(id), + ); + } +} diff --git a/apps/im_app/lib/data/repositories/chat_bot_repository_impl.dart b/apps/im_app/lib/data/repositories/chat_bot_repository_impl.dart new file mode 100644 index 0000000..0719286 --- /dev/null +++ b/apps/im_app/lib/data/repositories/chat_bot_repository_impl.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'; +import 'package:im_app/domain/repositories/chat_bot_repository.dart'; +import 'package:storage_sdk/storage_sdk.dart'; + +/// 聊天机器人仓储实现 +/// +/// ## 职责 +/// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase +/// - DriftChatBot ↔ Domain ChatBot 映射 +/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在此类 +/// +/// ## 数据流 +/// ``` +/// 网络:ChatBot.fromJson(json) → insertOrReplaceChatBot(chatBot) → StorageSdkApi → DB +/// 监听:StorageSdkApi.watchAll → DriftChatBot → _toEntity() → UI +/// ``` +class ChatBotRepositoryImpl implements ChatBotRepository { + final StorageSdkApi _storage; + + ChatBotRepositoryImpl(this._storage); + + // ── DB row → Domain ────────────────────────────────────────────────────── + + ChatBot _toEntity(DriftChatBot row) => ChatBot( + id: row.id, + name: row.name, + username: row.username, + botUserId: row.botUserId, + icon: row.icon, + iconGaussian: row.iconGaussian, + description: row.description, + token: row.token, + flag: row.flag, + status: row.status, + webhook: row.webhook, + commands: row.commands, + banner: row.banner, + channelId: row.channelId, + channelGroupId: row.channelGroupId, + deletedAt: row.deletedAt, + internalWebhook: row.internalWebhook, + mode: row.mode, + redirectUrl: row.redirectUrl, + isInvitable: row.isInvitable, + isAllowForward: row.isAllowForward, + tips: row.tips, + ); + + // ── Domain → DB companion (internal only) ──────────────────────────────── + + ChatBotsCompanion _toCompanion(ChatBot chatBot) => ChatBotsCompanion( + id: Value(chatBot.id), + name: Value(chatBot.name), + username: Value(chatBot.username), + botUserId: Value(chatBot.botUserId), + icon: Value(chatBot.icon), + iconGaussian: Value(chatBot.iconGaussian), + description: Value(chatBot.description), + token: Value(chatBot.token), + flag: Value(chatBot.flag), + status: Value(chatBot.status), + webhook: Value(chatBot.webhook ?? ''), + commands: Value(chatBot.commands ?? '[]'), + banner: Value(chatBot.banner), + channelId: Value(chatBot.channelId), + channelGroupId: Value(chatBot.channelGroupId), + deletedAt: Value(chatBot.deletedAt), + internalWebhook: Value(chatBot.internalWebhook), + mode: Value(chatBot.mode), + redirectUrl: Value(chatBot.redirectUrl), + isInvitable: Value(chatBot.isInvitable), + isAllowForward: Value(chatBot.isAllowForward), + tips: Value(chatBot.tips), + ); + + // ── 监听 ───────────────────────────────────────────────────────────────── + + @override + Stream> watchAllChatBots() { + return _storage.watchAll().map( + (rows) => rows.map(_toEntity).toList(), + ); + } + + @override + Stream watchChatBot(int id) { + return _storage + .watchFirst((t) => t.id.equals(id)) + .map((row) => row != null ? _toEntity(row) : null); + } + + // ── 读取 ───────────────────────────────────────────────────────────────── + + @override + Future> getChatBots({int? limit}) async { + if (limit != null) { + final rows = await _storage.rawQuery('SELECT * FROM chat_bot LIMIT ?', [ + limit, + ]); + return rows + .map( + (row) => ChatBot( + id: row.read('id'), + name: row.readNullable('name'), + username: row.readNullable('username'), + botUserId: row.readNullable('bot_user_id'), + icon: row.readNullable('icon'), + iconGaussian: row.readNullable('icon_gaussian'), + description: row.readNullable('description'), + token: row.readNullable('token'), + flag: row.readNullable('flag'), + status: row.readNullable('status'), + webhook: row.readNullable('webhook'), + commands: row.readNullable('commands'), + banner: row.readNullable('banner'), + channelId: row.readNullable('channel_id'), + channelGroupId: row.readNullable('channel_group_id'), + deletedAt: row.readNullable('deleted_at'), + internalWebhook: row.readNullable('internal_webhook'), + mode: row.readNullable('mode'), + redirectUrl: row.readNullable('redirect_url'), + isInvitable: row.readNullable('is_invitable'), + isAllowForward: row.readNullable('is_allow_forward'), + tips: row.readNullable('tips'), + ), + ) + .toList(); + } + + final rows = await _storage.selectAll(); + return rows.map(_toEntity).toList(); + } + + @override + Future getChatBot(int id) async { + final row = await _storage.selectFirst( + (t) => t.id.equals(id), + ); + return row != null ? _toEntity(row) : null; + } + + // ── 写入 ───────────────────────────────────────────────────────────────── + + @override + Future insertOrReplaceChatBot(ChatBot chatBot) async { + await _storage.insertOrReplace(_toCompanion(chatBot)); + } + + @override + Future insertOrReplaceChatBots(List chatBots) async { + await _storage.batchInsertOrReplace( + chatBots.map(_toCompanion).toList(), + ); + } + + @override + Future updateChatBot(ChatBot chatBot) async { + await _storage.updateWhere( + _toCompanion(chatBot), + (t) => t.id.equals(chatBot.id), + ); + } + + // ── 删除 ───────────────────────────────────────────────────────────────── + + @override + Future deleteChatBot(int id) async { + await _storage.deleteWhere( + (t) => t.id.equals(id), + ); + } + + @override + Future clearChatBots() async { + await _storage.deleteAll(); + } +} diff --git a/apps/im_app/lib/data/repositories/user_repository_impl.dart b/apps/im_app/lib/data/repositories/user_repository_impl.dart index 10fbc74..b32f153 100644 --- a/apps/im_app/lib/data/repositories/user_repository_impl.dart +++ b/apps/im_app/lib/data/repositories/user_repository_impl.dart @@ -9,13 +9,13 @@ import 'package:storage_sdk/storage_sdk.dart'; /// ## 职责 /// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase /// - DriftUser ↔ Domain User 映射 -/// - 持久化决策由调用方决定 +/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在此类 /// /// ## 数据流 /// ``` -/// 网络:User.fromJson(json) → (可选) saveUser(user) → StorageSdkApi → DB +/// 网络:User.fromJson(json) → (可选) insertOrReplaceUser(user) → StorageSdkApi → DB /// 监听:StorageSdkApi.watchWhere/watchAll → DriftUser → _toEntity() → UI -/// 部分更新:updateFields(uid, UsersCompanion) → StorageSdkApi.updateWhere +/// 更新:updateUser(user) / updateUsersBatch(users) → _toCompanion() → DB /// ``` class UserRepositoryImpl implements UserRepository { final StorageSdkApi _storage; @@ -60,7 +60,7 @@ class UserRepositoryImpl implements UserRepository { hint: row.hint, ); - // ── Domain → DB companion ──────────────────────────────────────────────── + // ── Domain → DB companion (internal only) ──────────────────────────────── UsersCompanion _toCompanion(User user) => UsersCompanion( uid: Value(user.uid), @@ -98,9 +98,92 @@ class UserRepositoryImpl implements UserRepository { hint: Value(user.hint), ); + UsersCompanion _toPartialCompanion(User user) => UsersCompanion( + uid: Value(user.uid), + uuid: user.uuid != null ? Value(user.uuid) : const Value.absent(), + lastOnline: user.lastOnline != null + ? Value(user.lastOnline) + : const Value.absent(), + profilePic: user.profilePic != null + ? Value(user.profilePic) + : const Value.absent(), + profilePicGaussian: user.profilePicGaussian != null + ? Value(user.profilePicGaussian!) + : const Value.absent(), + nickname: user.nickname != null + ? Value(user.nickname) + : const Value.absent(), + depositName: user.depositName != null + ? Value(user.depositName) + : const Value.absent(), + hasSetDepositName: user.hasSetDepositName != null + ? Value(user.hasSetDepositName!) + : const Value.absent(), + contact: user.contact != null ? Value(user.contact) : const Value.absent(), + countryCode: user.countryCode != null + ? Value(user.countryCode) + : const Value.absent(), + username: user.username != null + ? Value(user.username) + : const Value.absent(), + role: user.role != null ? Value(user.role) : const Value.absent(), + relationship: user.relationship != null + ? Value(user.relationship) + : const Value.absent(), + friendStatus: user.friendStatus != null + ? Value(user.friendStatus) + : const Value.absent(), + bio: user.bio != null ? Value(user.bio) : const Value.absent(), + userAlias: user.userAlias != null + ? Value(user.userAlias) + : const Value.absent(), + requestAt: user.requestAt != null + ? Value(user.requestAt) + : const Value.absent(), + deletedAt: user.deletedAt != null + ? Value(user.deletedAt) + : const Value.absent(), + email: user.email != null ? Value(user.email) : const Value.absent(), + recoveryEmail: user.recoveryEmail != null + ? Value(user.recoveryEmail) + : const Value.absent(), + remark: user.remark != null ? Value(user.remark) : const Value.absent(), + source: user.source != null ? Value(user.source) : const Value.absent(), + addIndex: user.addIndex != null + ? Value(user.addIndex) + : const Value.absent(), + incomingSoundId: user.incomingSoundId != null + ? Value(user.incomingSoundId!) + : const Value.absent(), + outgoingSoundId: user.outgoingSoundId != null + ? Value(user.outgoingSoundId!) + : const Value.absent(), + notificationSoundId: user.notificationSoundId != null + ? Value(user.notificationSoundId!) + : const Value.absent(), + sendMessageSoundId: user.sendMessageSoundId != null + ? Value(user.sendMessageSoundId!) + : const Value.absent(), + groupNotificationSoundId: user.groupNotificationSoundId != null + ? Value(user.groupNotificationSoundId!) + : const Value.absent(), + groupTags: user.groupTags != null + ? Value(user.groupTags!) + : const Value.absent(), + friendTags: user.friendTags != null + ? Value(user.friendTags!) + : const Value.absent(), + publicKey: user.publicKey != null + ? Value(user.publicKey) + : const Value.absent(), + configBits: user.configBits != null + ? Value(user.configBits!) + : const Value.absent(), + hint: user.hint != null ? Value(user.hint) : const Value.absent(), + ); + // ── 监听 ───────────────────────────────────────────────────────────────── - /// 监听单个用户 @override Stream watchUser(int uid) { return _storage @@ -108,7 +191,6 @@ class UserRepositoryImpl implements UserRepository { .map((row) => row != null ? _toEntity(row) : null); } - /// 监听指定 uid 列表 @override Stream> watchUsers(List uids) { return _storage @@ -116,7 +198,6 @@ class UserRepositoryImpl implements UserRepository { .map((rows) => rows.map(_toEntity).toList()); } - /// 监听所有用户 @override Stream> watchAllUsers() { return _storage.watchAll().map( @@ -199,34 +280,80 @@ class UserRepositoryImpl implements UserRepository { // ── 写入 ───────────────────────────────────────────────────────────────── @override - Future saveUser(User user) async { - await _storage.insertOrReplace(_toCompanion(user)); + Future insertOrReplaceUser(User user) async { + if (user.requireUpsert) { + await _storage.insertOrReplace(_toCompanion(user)); + } } @override - Future saveUsers(List users) async { - await _storage.batchInsertOrReplace( - users.map(_toCompanion).toList(), - ); + Future insertOrReplaceUsers(List users) async { + final List upsertList = []; + final List insertList = []; + for (final user in users) { + user.requireUpsert ? upsertList.add(user) : insertList.add(user); + } + + if (insertList.isNotEmpty) { + await _storage.batchInsertOrReplace( + insertList.map(_toCompanion).toList(), + ); + } + if (upsertList.isNotEmpty) { + await upsertUsers(upsertList); + } } - /// 仅更新指定列,其他列不变 - /// - /// 示例: - /// ```dart - /// await userRepo.updateFields(uid, UsersCompanion( - /// nickname: Value('New Name'), - /// lastOnline: Value(DateTime.now().millisecondsSinceEpoch), - /// )); - /// ``` @override - Future updateFields(int uid, UsersCompanion companion) async { + Future updateUser(User user) async { await _storage.updateWhere( - companion, - (t) => t.uid.equals(uid), + _toCompanion(user), + (t) => t.uid.equals(user.uid), ); } + /// 批量更新已存在的用户记录。 + /// + /// 仅更新,不插入——调用方应确保这些用户已存在于 DB 中。 + /// 如需 upsert 语义,请使用 [upsertUsers]。 + /// + /// 所有更新在同一事务内完成,保证原子性。 + @override + Future updateUsersBatch(List users) async { + await _storage.transaction(() async { + for (final user in users) { + await updateUser(user); + } + }); + } + + /// 单条 upsert:不存在则插入,存在则仅更新非 null 字段。 + /// + /// 内部委托给 [upsertUsers]。 + @override + Future upsertUser(User user) => upsertUsers([user]); + + /// 批量 upsert:不存在则插入,存在则仅更新非 null 字段。 + /// + /// 与 [insertOrReplaceUsers] 的区别: + /// - [insertOrReplaceUsers] → INSERT OR REPLACE,全字段覆盖 + /// - [upsertUsers] → INSERT OR IGNORE + UPDATE,仅更新传入的非 null 字段 + /// + /// 所有操作在同一事务内完成,保证原子性。 + /// 适用场景:从服务端收到部分用户信息,不希望覆盖本地已有的其他字段。 + @override + Future upsertUsers(List users) async { + await _storage.transaction(() async { + for (final user in users) { + await _storage.insert(_toCompanion(user)); + await _storage.updateWhere( + _toPartialCompanion(user), + (t) => t.uid.equals(user.uid), + ); + } + }); + } + // ── 删除 ───────────────────────────────────────────────────────────────── @override diff --git a/apps/im_app/lib/domain/entities/company_member.dart b/apps/im_app/lib/domain/entities/company_member.dart new file mode 100644 index 0000000..fb139fd --- /dev/null +++ b/apps/im_app/lib/domain/entities/company_member.dart @@ -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 json) => CompanyMember( + uid: json['user_id'] as int, + nickname: json['name'], + lastOnline: json['last_online'], + ); +} diff --git a/apps/im_app/lib/domain/entities/group_member.dart b/apps/im_app/lib/domain/entities/group_member.dart new file mode 100644 index 0000000..3386594 --- /dev/null +++ b/apps/im_app/lib/domain/entities/group_member.dart @@ -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 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 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, + }; +} diff --git a/apps/im_app/lib/domain/entities/user.dart b/apps/im_app/lib/domain/entities/user.dart index 7e29144..ed9697e 100644 --- a/apps/im_app/lib/domain/entities/user.dart +++ b/apps/im_app/lib/domain/entities/user.dart @@ -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 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, ); } } diff --git a/apps/im_app/lib/domain/repositories/call_log_repository.dart b/apps/im_app/lib/domain/repositories/call_log_repository.dart new file mode 100644 index 0000000..e5f019d --- /dev/null +++ b/apps/im_app/lib/domain/repositories/call_log_repository.dart @@ -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> watchAllCallLogs(); + + /// 监听指定通话记录 + Stream watchCallLog(String id); + + // ── 读取 ───────────────────────────────────────────────────────────────── + + /// 读取所有通话记录,按 updated_at 降序 + Future> getCallLogs(); + + /// 读取指定通话记录,不存在返回 null + Future getCallLog(String id); + + /// 检查通话记录是否存在 + Future isExist(String id); + + /// 获取未读通话数量 + /// + /// 统计 is_read = 0 且非主叫的未接/取消/结束/超时通话 + Future getUnreadCount(int currentUid); + + // ── 写入 ───────────────────────────────────────────────────────────────── + + /// 插入或替换通话记录 + Future insertOrReplaceCallLog(CallLog callLog); + + /// 批量插入或替换通话记录 + Future insertOrReplaceCallLogs(List callLogs); + + /// 将所有通话记录标记为已读 + Future markAllAsRead(); + + // ── 删除 ───────────────────────────────────────────────────────────────── + + /// 删除指定通话记录 + Future deleteCallLog(String id); +} diff --git a/apps/im_app/lib/domain/repositories/chat_bot_repository.dart b/apps/im_app/lib/domain/repositories/chat_bot_repository.dart new file mode 100644 index 0000000..6765ff2 --- /dev/null +++ b/apps/im_app/lib/domain/repositories/chat_bot_repository.dart @@ -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> watchAllChatBots(); + + /// 监听指定聊天机器人 + Stream watchChatBot(int id); + + // ── 读取 ───────────────────────────────────────────────────────────────── + + /// 读取所有聊天机器人 + /// + /// [limit] 可选限制数量 + Future> getChatBots({int? limit}); + + /// 读取指定聊天机器人,不存在返回 null + Future getChatBot(int id); + + // ── 写入 ───────────────────────────────────────────────────────────────── + + /// 插入或替换聊天机器人 + Future insertOrReplaceChatBot(ChatBot chatBot); + + /// 批量插入或替换聊天机器人 + Future insertOrReplaceChatBots(List chatBots); + + /// 更新聊天机器人 + Future updateChatBot(ChatBot chatBot); + + // ── 删除 ───────────────────────────────────────────────────────────────── + + /// 删除指定聊天机器人 + Future deleteChatBot(int id); + + /// 清空所有聊天机器人 + Future clearChatBots(); +} diff --git a/apps/im_app/lib/domain/repositories/user_repository.dart b/apps/im_app/lib/domain/repositories/user_repository.dart index 3fa4571..40a4106 100644 --- a/apps/im_app/lib/domain/repositories/user_repository.dart +++ b/apps/im_app/lib/domain/repositories/user_repository.dart @@ -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 saveUser(User user); + /// 插入或替换单个用户 + Future insertOrReplaceUser(User user); - /// 批量保存用户(insert or replace) - Future saveUsers(List users); + /// 批量插入或替换用户 + Future insertOrReplaceUsers(List 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 updateFields(int uid, UsersCompanion companion); + Future updateUser(User user); + + /// 批量更新用户,每条按 uid 匹配更新所有字段 + /// + /// 示例: + /// ```dart + /// final updated = users.map((u) => u.copyWith(nickname: 'new')).toList(); + /// await repo.updateUsersBatch(updated); + /// ``` + Future updateUsersBatch(List users); + + /// 单条 upsert + Future upsertUser(User user); + + /// 批量 upsert,走单次事务,优于循环调用 [upsertUser] + Future upsertUsers(List users); // ── 删除 ───────────────────────────────────────────────────────────────── diff --git a/apps/im_app/lib/domain/usecases/insert_users_use_case.dart b/apps/im_app/lib/domain/usecases/insert_users_use_case.dart deleted file mode 100644 index 7223b1b..0000000 --- a/apps/im_app/lib/domain/usecases/insert_users_use_case.dart +++ /dev/null @@ -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 execute( - List users, { - void Function(int completed, int total, List 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; - } -} diff --git a/apps/im_app/lib/features/chat/call/di/call_log_provider.dart b/apps/im_app/lib/features/chat/call/di/call_log_provider.dart new file mode 100644 index 0000000..10a94bc --- /dev/null +++ b/apps/im_app/lib/features/chat/call/di/call_log_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:im_app/app/di/db_provider.dart'; +import 'package:im_app/data/repositories/call_log_repository_impl.dart'; +import 'package:im_app/domain/entities/call_log.dart'; +import 'package:im_app/domain/repositories/call_log_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'call_log_provider.g.dart'; + +// ── Repository ──────────────────────────────────────────────────────────────── + +/// 通话记录仓储 Provider +final callLogRepositoryProvider = Provider((ref) { + return CallLogRepositoryImpl(ref.watch(storageSdkProvider)); +}); + +// ── Streams ─────────────────────────────────────────────────────────────────── + +/// 监听所有通话记录 +@riverpod +Stream> allCallLogs(Ref ref) { + return ref.watch(callLogRepositoryProvider).watchAllCallLogs(); +} + +/// 监听指定通话记录 +@riverpod +Stream callLog(Ref ref, String id) { + return ref.watch(callLogRepositoryProvider).watchCallLog(id); +} diff --git a/apps/im_app/lib/features/chat/di/chat_bot_provider.dart b/apps/im_app/lib/features/chat/di/chat_bot_provider.dart new file mode 100644 index 0000000..744d7b2 --- /dev/null +++ b/apps/im_app/lib/features/chat/di/chat_bot_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:im_app/app/di/db_provider.dart'; +import 'package:im_app/data/repositories/chat_bot_repository_impl.dart'; +import 'package:im_app/domain/entities/chat_bot.dart'; +import 'package:im_app/domain/repositories/chat_bot_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'chat_bot_provider.g.dart'; + +// ── Repository ──────────────────────────────────────────────────────────────── + +/// 聊天机器人仓储 Provider +final chatBotRepositoryProvider = Provider((ref) { + return ChatBotRepositoryImpl(ref.watch(storageSdkProvider)); +}); + +// ── Streams ─────────────────────────────────────────────────────────────────── + +/// 监听所有聊天机器人 +@riverpod +Stream> allChatBots(Ref ref) { + return ref.watch(chatBotRepositoryProvider).watchAllChatBots(); +} + +/// 监听指定聊天机器人 +@riverpod +Stream chatBot(Ref ref, int id) { + return ref.watch(chatBotRepositoryProvider).watchChatBot(id); +} 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 e9a60e9..3679f9f 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 @@ -87,7 +87,7 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel { _loadNextPage(); } - // ── 测试 ────────────────────────────────────────────────────────────────── + // ── 测试:插入 ──────────────────────────────────────────────────────────── void startDBTest(BuildContext context) { _isTesting = true; @@ -147,4 +147,67 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel { ); } } + + // ── 测试:更新 ──────────────────────────────────────────────────────────── + + void updateFirst10(BuildContext context) { + _testDBUpdate(); + } + + Future _testDBUpdate() async { + final useCase = ref.read(updateUsersUseCaseProvider); + final users = state.users; + + if (users.isEmpty) { + state = state.copyWith(currentState: '暂无数据,请先插入'); + return; + } + + final updated = await useCase.execute(users); + + if (ref.mounted) { + final updatedMap = {for (final u in updated) u.uid: u}; + final updatedList = state.users + .map((u) => updatedMap[u.uid] ?? u) + .toList(); + + state = state.copyWith( + users: updatedList, + currentState: '已更新前 ${updated.length} 条', + ); + } + } + + // ── 测试:删除 ──────────────────────────────────────────────────────────── + + void deleteFirst10(BuildContext context) { + _testDBDelete(); + } + + Future _testDBDelete() async { + final useCase = ref.read(deleteUsersUseCaseProvider); + final repo = ref.read(userRepositoryProvider); + final users = state.users; + + if (users.isEmpty) { + state = state.copyWith(currentState: '暂无数据,请先插入'); + return; + } + + final deleted = await useCase.execute(users); + + if (ref.mounted) { + final deletedUids = {for (final u in deleted) u.uid}; + final updatedList = state.users + .where((u) => !deletedUids.contains(u.uid)) + .toList(); + + final total = await repo.countUsers(); + state = state.copyWith( + users: updatedList, + totalCount: total, + currentState: '已删除前 ${deleted.length} 条', + ); + } + } } diff --git a/apps/im_app/lib/features/chat/usecases/.gitkeep b/apps/im_app/lib/features/chat/usecases/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/im_app/lib/features/chat/usecases/delete_users_use_case.dart b/apps/im_app/lib/features/chat/usecases/delete_users_use_case.dart new file mode 100644 index 0000000..5348895 --- /dev/null +++ b/apps/im_app/lib/features/chat/usecases/delete_users_use_case.dart @@ -0,0 +1,39 @@ +import 'package:im_app/domain/entities/user.dart'; +import 'package:im_app/domain/repositories/user_repository.dart'; + +/// 删除前10个用户用例 +/// +/// ## 职责 +/// - 取前10个用户 +/// - 按 uid 逐条删除 +/// +/// ## 数据流 +/// ``` +/// ViewModel +/// → DeleteUsersUseCase.execute(users) +/// → 取前10条 +/// → UserRepository.deleteUser(uid) × n +/// ← 已删除的用户列表 +/// ``` +class DeleteUsersUseCase { + final UserRepository _repo; + + DeleteUsersUseCase({required UserRepository userRepository}) + : _repo = userRepository; + + /// 删除前10个用户 + /// + /// [users] 当前用户列表,取前10条删除 + /// 返回已删除的用户列表,供 ViewModel 从 UI 中移除 + Future> execute(List users) async { + if (users.isEmpty) return []; + + final targets = users.take(10).toList(); + + for (final user in targets) { + await _repo.deleteUser(user.uid); + } + + return targets; + } +} diff --git a/apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart b/apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart index 7223b1b..7692d95 100644 --- a/apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart +++ b/apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart @@ -44,7 +44,7 @@ class InsertUsersUseCase { final end = (completed + _chunkSize).clamp(0, total); final chunk = deduped.sublist(completed, end); - await _repo.saveUsers(chunk); + await _repo.insertOrReplaceUsers(chunk); completed += chunk.length; onProgress?.call(completed, total, chunk); diff --git a/apps/im_app/lib/features/chat/usecases/update_users_use_case.dart b/apps/im_app/lib/features/chat/usecases/update_users_use_case.dart new file mode 100644 index 0000000..a09ae46 --- /dev/null +++ b/apps/im_app/lib/features/chat/usecases/update_users_use_case.dart @@ -0,0 +1,53 @@ +import 'dart:math'; + +import 'package:im_app/domain/entities/user.dart'; +import 'package:im_app/domain/repositories/user_repository.dart'; + +/// 更新前10个用户名称用例 +/// +/// ## 职责 +/// - 取前10个用户 +/// - 随机生成新昵称(6位随机字母 + uid) +/// - 批量更新到 DB +/// +/// ## 数据流 +/// ``` +/// ViewModel +/// → UpdateUsersUseCase.execute(users) +/// → 取前10条 +/// → 随机生成昵称 +/// → UserRepository.updateUsersBatch(targets) +/// ← 更新后的用户列表 +/// ``` +class UpdateUsersUseCase { + final UserRepository _repo; + final _random = Random(); + + UpdateUsersUseCase({required UserRepository userRepository}) + : _repo = userRepository; + + String _randomWord() { + const letters = 'abcdefghijklmnopqrstuvwxyz'; + return List.generate( + 6, + (_) => letters[_random.nextInt(letters.length)], + ).join(); + } + + /// 更新前10个用户的昵称 + /// + /// [users] 当前用户列表,取前10条更新 + /// 返回更新后的前10个用户,供 ViewModel 直接反映到 UI + Future> execute(List users) async { + if (users.isEmpty) return []; + + final targets = users + .take(10) + .map((u) => u.copyWith(nickname: '${_randomWord()}_${u.uid}')) + .toList(); + + await _repo.updateUsersBatch(targets); + + return targets; + } +} 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 fe3f90f..70da5b4 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 @@ -62,11 +62,25 @@ class _ChatDbTestPageState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - AppButton.inverse( - label: testStarted ? '结束' : '开始', - onPressed: () => testStarted - ? vm.stopDBTest(context) - : vm.startDBTest(context), + Column( + children: [ + AppButton.inverse( + label: "插入1万条数据", + onPressed: () => testStarted + ? vm.stopDBTest(context) + : vm.startDBTest(context), + ), + const SizedBox(height: 4), + AppButton.inverse( + label: '编辑前10条名称', + onPressed: () => vm.updateFirst10(context), + ), + const SizedBox(height: 4), + AppButton.inverse( + label: '删除前10条名称', + onPressed: () => vm.deleteFirst10(context), + ), + ], ), const SizedBox(width: 8), Expanded(child: Text(currentState, textAlign: TextAlign.end)), 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 3ae37a0..674fd15 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 @@ -97,7 +97,7 @@ class LoginViewModel extends _$LoginViewModel { // 先完成 DB 操作,再标记登录状态(失败时不会误标为已登录) await storageLifeCycle.openDatabase(user.uid); // Save user to DB via repository - await repositoryProvider.saveUser(user); + await repositoryProvider.insertOrReplaceUser(user); // Trigger auth state provider.login(); From e29caed253feb752629c6c9310e9451e5aa1975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Happi=20=28=E5=93=88=E6=AF=94=29?= Date: Mon, 9 Mar 2026 19:39:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/facade/storage_sdk_api.dart | 57 +++++++++------ .../wiring/storage_sdk_api_impl.dart | 73 +++++++++++-------- 2 files changed, 76 insertions(+), 54 deletions(-) diff --git a/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart b/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart index 70b2015..d6e6861 100644 --- a/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart +++ b/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart @@ -56,11 +56,26 @@ abstract class StorageSdkApi { factory StorageSdkApi({ required GeneratedDatabase Function(QueryExecutor) databaseFactory, required Map Function(GeneratedDatabase) tableRegistry, - }) => - StorageSdkWiring.build( - databaseFactory: databaseFactory, - tableRegistry: tableRegistry, - ); + }) => StorageSdkWiring.build( + databaseFactory: databaseFactory, + tableRegistry: tableRegistry, + ); + + // ── 事务 ───────────────────────────────────────────────────────────────── + + /// 在单个事务内执行 [action]。 + /// + /// - 成功则自动 COMMIT + /// - 抛出异常则自动 ROLLBACK,异常会继续向上传递 + /// + /// 示例: + /// ```dart + /// await _storage.transaction(() async { + /// await _storage.insertOrReplace(companionA); + /// await _storage.updateWhere(companionB, (t) => t.uid.equals(1)); + /// }); + /// ``` + Future transaction(Future Function() action); // ── 插入 ───────────────────────────────────────────────────────────────── @@ -92,16 +107,16 @@ abstract class StorageSdkApi { /// ); /// ``` Future updateWhere( - Insertable companion, - Expression Function(T) filter, - ); + Insertable companion, + Expression Function(T) filter, + ); // ── 删除 ───────────────────────────────────────────────────────────────── /// 按条件删除。 Future deleteWhere( - Expression Function(T) filter, - ); + Expression Function(T) filter, + ); /// 清空整张表。 Future deleteAll(); @@ -118,13 +133,13 @@ abstract class StorageSdkApi { /// 按条件查询。 Future> selectWhere( - Expression Function(T) filter, - ); + Expression Function(T) filter, + ); /// 查询第一条匹配记录。 Future selectFirst( - Expression Function(T) filter, - ); + Expression Function(T) filter, + ); // ── 监听 ───────────────────────────────────────────────────────────────── @@ -133,13 +148,13 @@ abstract class StorageSdkApi { /// 按条件监听(实时流)。 Stream> watchWhere( - Expression Function(T) filter, - ); + Expression Function(T) filter, + ); /// 监听第一条匹配记录(实时流)。 Stream watchFirst( - Expression Function(T) filter, - ); + Expression Function(T) filter, + ); // ── 原始 SQL ───────────────────────────────────────────────────────────── @@ -152,7 +167,5 @@ abstract class StorageSdkApi { // ── 统计 ───────────────────────────────────────────────────────────────── /// 统计记录数。 - Future count({ - Expression Function(T)? filter, - }); -} \ No newline at end of file + Future count({Expression Function(T)? filter}); +} diff --git a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart index cdbf4cb..a5e8cba 100644 --- a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart +++ b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart @@ -11,8 +11,8 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { StorageSdkApiImpl({ required StorageSdkCore core, required Map Function(GeneratedDatabase) tableRegistry, - }) : _core = core, - _tableRegistry = tableRegistry; + }) : _core = core, + _tableRegistry = tableRegistry; // ── 表查找 ─────────────────────────────────────────────────────────────── @@ -25,6 +25,13 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { return table as TableInfo; } + /// 获取当前已开启的数据库实例,未开启则抛出 [StateError]。 + GeneratedDatabase get _db { + final db = _core.dataSource.current; + if (db == null) throw StateError('数据库未开启,请先调用 openDatabase()'); + return db; + } + // ── 生命周期 ───────────────────────────────────────────────────────────── @override @@ -36,6 +43,16 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { @override bool get isDatabaseOpen => _core.dataSource.current != null; + // ── 事务 ───────────────────────────────────────────────────────────────── + + /// 在单个事务内执行 [action]。 + /// + /// - 成功则自动 COMMIT + /// - 抛出异常则自动 ROLLBACK,异常会继续向上传递 + @override + Future transaction(Future Function() action) => + _db.transaction(action); + // ── 插入 ───────────────────────────────────────────────────────────────── @override @@ -54,64 +71,57 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { @override Future updateWhere( - Insertable companion, - Expression Function(T) filter, - ) => - _core.repo.updateWhere(_tableFor(), companion, filter); + Insertable companion, + Expression Function(T) filter, + ) => _core.repo.updateWhere(_tableFor(), companion, filter); // ── 删除 ───────────────────────────────────────────────────────────────── @override Future deleteWhere( - Expression Function(T) filter, - ) => - _core.repo.deleteWhere(_tableFor(), filter); + Expression Function(T) filter, + ) => _core.repo.deleteWhere(_tableFor(), filter); @override - Future deleteAll() => - _core.repo.deleteAll(_tableFor()); + Future deleteAll() => _core.repo.deleteAll(_tableFor()); // ── 查询 ───────────────────────────────────────────────────────────────── @override - Future> selectAll() => - _core.repo.selectAll(_tableFor()); + Future> selectAll() => _core.repo.selectAll(_tableFor()); @override Future> selectWhere( - Expression Function(T) filter, - ) => - _core.repo.selectWhere(_tableFor(), filter); + Expression Function(T) filter, + ) => _core.repo.selectWhere(_tableFor(), filter); @override Future selectFirst( - Expression Function(T) filter, - ) => - _core.repo.selectFirst(_tableFor(), filter); + Expression Function(T) filter, + ) => _core.repo.selectFirst(_tableFor(), filter); // ── 监听 ───────────────────────────────────────────────────────────────── @override - Stream> watchAll() => - _core.repo.watchAll(_tableFor()); + Stream> watchAll() => _core.repo.watchAll(_tableFor()); @override Stream> watchWhere( - Expression Function(T) filter, - ) => - _core.repo.watchWhere(_tableFor(), filter); + Expression Function(T) filter, + ) => _core.repo.watchWhere(_tableFor(), filter); @override Stream watchFirst( - Expression Function(T) filter, - ) => - _core.repo.watchFirst(_tableFor(), filter); + Expression Function(T) filter, + ) => _core.repo.watchFirst(_tableFor(), filter); // ── 原始 SQL ───────────────────────────────────────────────────────────── @override - Future> rawQuery(String sql, [List args = const []]) => - _core.repo.rawQuery(sql, args); + Future> rawQuery( + String sql, [ + List args = const [], + ]) => _core.repo.rawQuery(sql, args); @override Future rawExecute(String sql, [List args = const []]) => @@ -122,6 +132,5 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { @override Future count({ Expression Function(T)? filter, - }) => - _core.repo.count(_tableFor(), filter: filter); -} \ No newline at end of file + }) => _core.repo.count(_tableFor(), filter: filter); +}