From 7b78da86e75575300e7d6cdfedb747440d2dcdc4 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 13:03:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=A1=88=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/im_app/lib/data/models/user_dto.dart | 148 ----------- .../lib/data/remote/get_profile_request.dart | 2 - .../im_app/lib/data/remote/login_request.dart | 4 +- .../repositories/user_repository_impl.dart | 238 ++++++++++++++++++ apps/im_app/lib/domain/entities/user.dart | 162 +++++++++--- .../domain/presentation/di/user_provider.dart | 28 +++ .../presentation/notifiers/user_notifier.dart | 69 +++++ .../domain/repositories/user_repository.dart | 78 ++++++ .../presentation/chat_db_test_view_model.dart | 132 ++++++---- .../features/chat/view/chat_db_test_page.dart | 116 ++++++--- .../login/presentation/login_view_model.dart | 22 +- 11 files changed, 721 insertions(+), 278 deletions(-) delete mode 100644 apps/im_app/lib/data/models/user_dto.dart create mode 100644 apps/im_app/lib/data/repositories/user_repository_impl.dart create mode 100644 apps/im_app/lib/domain/presentation/di/user_provider.dart create mode 100644 apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart create mode 100644 apps/im_app/lib/domain/repositories/user_repository.dart diff --git a/apps/im_app/lib/data/models/user_dto.dart b/apps/im_app/lib/data/models/user_dto.dart deleted file mode 100644 index beb55c0..0000000 --- a/apps/im_app/lib/data/models/user_dto.dart +++ /dev/null @@ -1,148 +0,0 @@ -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] 之间的双向转换。 -class UserDto { - final int uid; - final String? uuid; - final int? lastOnline; - final String? profilePic; - final String? profilePicGaussian; - final String? nickname; - 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.uid, - this.uuid, - this.lastOnline, - this.profilePic, - this.profilePicGaussian, - this.nickname, - 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) => 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() => { - '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() => 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) => 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), - ); -} \ No newline at end of file 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 5398026..688322b 100644 --- a/apps/im_app/lib/data/remote/get_profile_request.dart +++ b/apps/im_app/lib/data/remote/get_profile_request.dart @@ -93,8 +93,6 @@ class ProfileData { bio: bio, relationship: relationship, userAlias: userAlias, - channelId: channelId, - channelGroupId: channelGroupId, hint: hint, ); } diff --git a/apps/im_app/lib/data/remote/login_request.dart b/apps/im_app/lib/data/remote/login_request.dart index 4145d71..910f8b0 100644 --- a/apps/im_app/lib/data/remote/login_request.dart +++ b/apps/im_app/lib/data/remote/login_request.dart @@ -88,8 +88,6 @@ class LoginProfile { bio: bio, relationship: relationship, userAlias: userAlias, - channelId: channelId, - channelGroupId: channelGroupId, hint: hint, ); } @@ -167,4 +165,4 @@ class LoginRequest extends ApiRequestable @override Map toJson() => _$LoginRequestToJson(this); -} \ No newline at end of file +} diff --git a/apps/im_app/lib/data/repositories/user_repository_impl.dart b/apps/im_app/lib/data/repositories/user_repository_impl.dart new file mode 100644 index 0000000..10fbc74 --- /dev/null +++ b/apps/im_app/lib/data/repositories/user_repository_impl.dart @@ -0,0 +1,238 @@ +import 'package:drift/drift.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'; +import 'package:storage_sdk/storage_sdk.dart'; + +/// 用户仓储实现 +/// +/// ## 职责 +/// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase +/// - DriftUser ↔ Domain User 映射 +/// - 持久化决策由调用方决定 +/// +/// ## 数据流 +/// ``` +/// 网络:User.fromJson(json) → (可选) saveUser(user) → StorageSdkApi → DB +/// 监听:StorageSdkApi.watchWhere/watchAll → DriftUser → _toEntity() → UI +/// 部分更新:updateFields(uid, UsersCompanion) → StorageSdkApi.updateWhere +/// ``` +class UserRepositoryImpl implements UserRepository { + final StorageSdkApi _storage; + + UserRepositoryImpl(this._storage); + + // ── DB row → Domain ────────────────────────────────────────────────────── + + User _toEntity(DriftUser row) => User( + uid: row.uid, + uuid: row.uuid, + lastOnline: row.lastOnline, + profilePic: row.profilePic, + profilePicGaussian: row.profilePicGaussian, + nickname: row.nickname, + depositName: row.depositName, + hasSetDepositName: row.hasSetDepositName, + contact: row.contact, + countryCode: row.countryCode, + username: row.username, + role: row.role, + relationship: row.relationship, + friendStatus: row.friendStatus, + bio: row.bio, + userAlias: row.userAlias, + requestAt: row.requestAt, + deletedAt: row.deletedAt, + email: row.email, + recoveryEmail: row.recoveryEmail, + remark: row.remark, + source: row.source, + addIndex: row.addIndex, + incomingSoundId: row.incomingSoundId, + outgoingSoundId: row.outgoingSoundId, + notificationSoundId: row.notificationSoundId, + sendMessageSoundId: row.sendMessageSoundId, + groupNotificationSoundId: row.groupNotificationSoundId, + groupTags: row.groupTags, + friendTags: row.friendTags, + publicKey: row.publicKey, + configBits: row.configBits, + hint: row.hint, + ); + + // ── Domain → DB companion ──────────────────────────────────────────────── + + UsersCompanion _toCompanion(User user) => UsersCompanion( + uid: Value(user.uid), + uuid: Value(user.uuid), + lastOnline: Value(user.lastOnline), + profilePic: Value(user.profilePic), + profilePicGaussian: Value(user.profilePicGaussian ?? ''), + nickname: Value(user.nickname), + depositName: Value(user.depositName), + hasSetDepositName: Value(user.hasSetDepositName ?? 0), + contact: Value(user.contact), + countryCode: Value(user.countryCode), + username: Value(user.username), + role: Value(user.role), + relationship: Value(user.relationship), + friendStatus: Value(user.friendStatus), + bio: Value(user.bio), + userAlias: Value(user.userAlias), + requestAt: Value(user.requestAt), + deletedAt: Value(user.deletedAt), + email: Value(user.email), + recoveryEmail: Value(user.recoveryEmail), + remark: Value(user.remark), + source: Value(user.source), + addIndex: Value(user.addIndex), + incomingSoundId: Value(user.incomingSoundId ?? 0), + outgoingSoundId: Value(user.outgoingSoundId ?? 0), + notificationSoundId: Value(user.notificationSoundId ?? 0), + sendMessageSoundId: Value(user.sendMessageSoundId ?? 0), + groupNotificationSoundId: Value(user.groupNotificationSoundId ?? 0), + groupTags: Value(user.groupTags ?? '[]'), + friendTags: Value(user.friendTags ?? '[]'), + publicKey: Value(user.publicKey), + configBits: Value(user.configBits ?? 0), + hint: Value(user.hint), + ); + + // ── 监听 ───────────────────────────────────────────────────────────────── + + /// 监听单个用户 + @override + Stream watchUser(int uid) { + return _storage + .watchFirst((t) => t.uid.equals(uid)) + .map((row) => row != null ? _toEntity(row) : null); + } + + /// 监听指定 uid 列表 + @override + Stream> watchUsers(List uids) { + return _storage + .watchWhere((t) => t.uid.isIn(uids)) + .map((rows) => rows.map(_toEntity).toList()); + } + + /// 监听所有用户 + @override + Stream> watchAllUsers() { + return _storage.watchAll().map( + (rows) => rows.map(_toEntity).toList(), + ); + } + + // ── 读取 ───────────────────────────────────────────────────────────────── + + @override + Future getUser(int uid) async { + final row = await _storage.selectFirst( + (t) => t.uid.equals(uid), + ); + return row != null ? _toEntity(row) : null; + } + + @override + Future> getAllUsers() async { + final rows = await _storage.selectAll(); + return rows.map(_toEntity).toList(); + } + + @override + Future> getUsers({required int offset, required int limit}) async { + final rows = await _storage.rawQuery( + 'SELECT * FROM user ORDER BY id LIMIT ? OFFSET ?', + [limit, offset], + ); + return rows + .map( + (row) => User( + uid: row.read('uid'), + uuid: row.readNullable('uuid'), + lastOnline: row.readNullable('last_online'), + profilePic: row.readNullable('profile_pic'), + profilePicGaussian: row.readNullable( + 'profile_pic_gaussian', + ), + nickname: row.readNullable('nickname'), + depositName: row.readNullable('deposit_name'), + hasSetDepositName: row.readNullable('has_set_deposit_name'), + contact: row.readNullable('contact'), + countryCode: row.readNullable('country_code'), + username: row.readNullable('username'), + role: row.readNullable('role'), + relationship: row.readNullable('relationship'), + friendStatus: row.readNullable('friend_status'), + bio: row.readNullable('bio'), + userAlias: row.readNullable('user_alias'), + requestAt: row.readNullable('request_at'), + deletedAt: row.readNullable('deleted_at'), + email: row.readNullable('email'), + recoveryEmail: row.readNullable('recovery_email'), + remark: row.readNullable('remark'), + source: row.readNullable('source'), + addIndex: row.readNullable('add_index'), + incomingSoundId: row.readNullable('incoming_sound_id'), + outgoingSoundId: row.readNullable('outgoing_sound_id'), + notificationSoundId: row.readNullable('notification_sound_id'), + sendMessageSoundId: row.readNullable('send_message_sound_id'), + groupNotificationSoundId: row.readNullable( + 'group_notification_sound_id', + ), + groupTags: row.readNullable('group_tags'), + friendTags: row.readNullable('friend_tags'), + publicKey: row.readNullable('public_key'), + configBits: row.readNullable('config_bits'), + hint: row.readNullable('hint'), + ), + ) + .toList(); + } + + @override + Future countUsers() async { + return _storage.count(); + } + + // ── 写入 ───────────────────────────────────────────────────────────────── + + @override + Future saveUser(User user) async { + await _storage.insertOrReplace(_toCompanion(user)); + } + + @override + Future saveUsers(List users) async { + await _storage.batchInsertOrReplace( + users.map(_toCompanion).toList(), + ); + } + + /// 仅更新指定列,其他列不变 + /// + /// 示例: + /// ```dart + /// await userRepo.updateFields(uid, UsersCompanion( + /// nickname: Value('New Name'), + /// lastOnline: Value(DateTime.now().millisecondsSinceEpoch), + /// )); + /// ``` + @override + Future updateFields(int uid, UsersCompanion companion) async { + await _storage.updateWhere( + companion, + (t) => t.uid.equals(uid), + ); + } + + // ── 删除 ───────────────────────────────────────────────────────────────── + + @override + Future deleteUser(int uid) async { + await _storage.deleteWhere( + (t) => t.uid.equals(uid), + ); + } +} diff --git a/apps/im_app/lib/domain/entities/user.dart b/apps/im_app/lib/domain/entities/user.dart index 0e15597..7e29144 100644 --- a/apps/im_app/lib/domain/entities/user.dart +++ b/apps/im_app/lib/domain/entities/user.dart @@ -3,13 +3,12 @@ /// 全局共享实体,被 auth / chat / contact 等多个 Feature 共用。 /// 纯 Dart 类,零 Flutter / 零网络 / 零 DB 依赖。 /// -/// ## 数据流位置 -/// +/// ## 数据流 /// ``` /// 服务端 JSON -/// → LoginData(Response DTO,data/remote/login_request.dart) -/// → LoginData.toEntity() -/// → ★ User ★ ← 你在这里 +/// → User.fromJson() ← 直接从网络创建 +/// → ★ User ★ ← 你在这里 +/// → userRepo.saveUser(user) ← 可选持久化 /// → ViewModel.state /// → View 渲染 /// ``` @@ -20,16 +19,32 @@ class User { final String? profilePic; final String? profilePicGaussian; final String? nickname; + final String? depositName; + final int? hasSetDepositName; final String? contact; final String? countryCode; + final String? username; + final int? role; + final int? relationship; + final int? friendStatus; + final String? bio; + final String? userAlias; + final int? requestAt; + final int? deletedAt; 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? remark; + final String? source; + final int? addIndex; + final int? incomingSoundId; + final int? outgoingSoundId; + final int? notificationSoundId; + final int? sendMessageSoundId; + final int? groupNotificationSoundId; + final String? groupTags; + final String? friendTags; + final String? publicKey; + final int? configBits; final String? hint; const User({ @@ -39,19 +54,73 @@ class User { this.profilePic, this.profilePicGaussian, this.nickname, + this.depositName, + this.hasSetDepositName, this.contact, this.countryCode, + this.username, + this.role, + this.relationship, + this.friendStatus, + this.bio, + this.userAlias, + this.requestAt, + this.deletedAt, this.email, this.recoveryEmail, - this.username, - this.bio, - this.relationship, - this.userAlias, - this.channelId, - this.channelGroupId, + this.remark, + this.source, + this.addIndex, + this.incomingSoundId, + this.outgoingSoundId, + this.notificationSoundId, + this.sendMessageSoundId, + this.groupNotificationSoundId, + this.groupTags, + this.friendTags, + this.publicKey, + this.configBits, this.hint, }); + /// 直接从网络 JSON 创建 Domain 实体 + factory User.fromJson(Map json) => User( + uid: json['uid'] as int, + uuid: json['uuid'], + lastOnline: json['last_online'], + profilePic: json['profile_pic'], + profilePicGaussian: json['profile_pic_gaussian'], + nickname: json['nickname'], + depositName: json['deposit_name'], + hasSetDepositName: json['has_set_deposit_name'], + contact: json['contact'], + countryCode: json['country_code'], + username: json['username'], + role: json['role'], + relationship: json['relationship'], + friendStatus: json['friend_status'], + bio: json['bio'], + userAlias: json['user_alias'], + requestAt: json['request_at'], + deletedAt: json['deleted_at'], + email: json['email'], + recoveryEmail: json['recovery_email'], + remark: json['remark'], + source: json['source'], + addIndex: json['__add_index'], + incomingSoundId: json['incoming_sound_id'], + outgoingSoundId: json['outgoing_sound_id'], + notificationSoundId: json['notification_sound_id'], + sendMessageSoundId: json['send_message_sound_id'], + groupNotificationSoundId: json['group_notification_sound_id'], + groupTags: json['group_tags'], + friendTags: json['friend_tags'], + publicKey: json['public_key'], + configBits: json['config_bits'], + hint: json['hint'], + ); + + /// 仅更新部分字段 User copyWith({ int? uid, String? uuid, @@ -59,16 +128,32 @@ class User { String? profilePic, String? profilePicGaussian, String? nickname, + String? depositName, + int? hasSetDepositName, String? contact, String? countryCode, + String? username, + int? role, + int? relationship, + int? friendStatus, + String? bio, + String? userAlias, + int? requestAt, + int? deletedAt, String? email, String? recoveryEmail, - String? username, - String? bio, - int? relationship, - String? userAlias, - int? channelId, - int? channelGroupId, + String? remark, + String? source, + int? addIndex, + int? incomingSoundId, + int? outgoingSoundId, + int? notificationSoundId, + int? sendMessageSoundId, + int? groupNotificationSoundId, + String? groupTags, + String? friendTags, + String? publicKey, + int? configBits, String? hint, }) { return User( @@ -78,17 +163,34 @@ class User { profilePic: profilePic ?? this.profilePic, profilePicGaussian: profilePicGaussian ?? this.profilePicGaussian, nickname: nickname ?? this.nickname, + depositName: depositName ?? this.depositName, + hasSetDepositName: hasSetDepositName ?? this.hasSetDepositName, contact: contact ?? this.contact, countryCode: countryCode ?? this.countryCode, + username: username ?? this.username, + role: role ?? this.role, + relationship: relationship ?? this.relationship, + friendStatus: friendStatus ?? this.friendStatus, + bio: bio ?? this.bio, + userAlias: userAlias ?? this.userAlias, + requestAt: requestAt ?? this.requestAt, + deletedAt: deletedAt ?? this.deletedAt, 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, + remark: remark ?? this.remark, + source: source ?? this.source, + addIndex: addIndex ?? this.addIndex, + incomingSoundId: incomingSoundId ?? this.incomingSoundId, + outgoingSoundId: outgoingSoundId ?? this.outgoingSoundId, + notificationSoundId: notificationSoundId ?? this.notificationSoundId, + sendMessageSoundId: sendMessageSoundId ?? this.sendMessageSoundId, + groupNotificationSoundId: + groupNotificationSoundId ?? this.groupNotificationSoundId, + groupTags: groupTags ?? this.groupTags, + friendTags: friendTags ?? this.friendTags, + publicKey: publicKey ?? this.publicKey, + configBits: configBits ?? this.configBits, hint: hint ?? this.hint, ); } -} \ No newline at end of file +} diff --git a/apps/im_app/lib/domain/presentation/di/user_provider.dart b/apps/im_app/lib/domain/presentation/di/user_provider.dart new file mode 100644 index 0000000..b9a823a --- /dev/null +++ b/apps/im_app/lib/domain/presentation/di/user_provider.dart @@ -0,0 +1,28 @@ +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'; +import 'package:im_app/domain/entities/user.dart'; +import 'package:im_app/domain/repositories/user_repository.dart'; + +part 'user_provider.g.dart'; + +// ── Repository ──────────────────────────────────────────────────────────────── + +@riverpod +UserRepository userRepository(Ref ref) { + return UserRepositoryImpl(ref.watch(storageSdkProvider)); +} + +// ── Multiple Users ──────────────────────────────────────────────────────────── + +@riverpod +Stream> users(Ref ref, Set uids) { + return ref.watch(userRepositoryProvider).watchUsers(uids.toList()); +} + +// ── All Users ───────────────────────────────────────────────────────────────── + +@riverpod +Stream> allUsers(Ref ref) { + return ref.watch(userRepositoryProvider).watchAllUsers(); +} diff --git a/apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart b/apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart new file mode 100644 index 0000000..2d2321a --- /dev/null +++ b/apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart @@ -0,0 +1,69 @@ +import 'package:im_app/domain/presentation/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'; + +part 'user_notifier.g.dart'; + +/// 单个用户状态管理 (family — 每个 uid 独立 notifier) +/// +/// ## 用法 +/// ```dart +/// // 监听 — 自动重建 +/// final userAsync = ref.watch(userNotifierProvider(123)); +/// +/// // 即时读取,无需 await +/// final user = ref.read(userNotifierProvider(123).notifier).current; +/// +/// // 部分更新 +/// ref.read(userNotifierProvider(123).notifier).updateFields( +/// UsersCompanion(nickname: Value('New Name')), +/// ); +/// ``` +@riverpod +class UserNotifier extends _$UserNotifier { + User? _cached; + + UserRepository get _repo => ref.watch(userRepositoryProvider); + + @override + 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); + } + + // ── 即时访问,无需 await ────────────────────────────────────────────────── + + User? get current => _cached; + + // ── 写入 ───────────────────────────────────────────────────────────────── + + Future saveUser(User user) async { + await _repo.saveUser(user); + } + + Future updateFields(UsersCompanion companion) async { + await _repo.updateFields(uid, companion); // uid from build arg + } + + Future updateUser(User user) async { + await _repo.saveUser(user); + } + + // ── 删除 ───────────────────────────────────────────────────────────────── + + Future deleteUser() async { + await _repo.deleteUser(uid); + _cached = null; + state = const AsyncData(null); + } +} diff --git a/apps/im_app/lib/domain/repositories/user_repository.dart b/apps/im_app/lib/domain/repositories/user_repository.dart new file mode 100644 index 0000000..3fa4571 --- /dev/null +++ b/apps/im_app/lib/domain/repositories/user_repository.dart @@ -0,0 +1,78 @@ +import 'package:im_app/data/local/drift/app_database.dart'; +import 'package:im_app/domain/entities/user.dart'; + +/// 用户仓储接口 +/// +/// ## 职责 +/// - StorageSdkApi ↔ Domain User 映射 +/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB) +/// - 实时监听(单个 / 多个 / 全部) +/// +/// ## 数据流 +/// ``` +/// 写入:Domain User → _toCompanion() → StorageSdkApi → DB +/// 读取:DB row (DriftUser) → _toEntity() → Domain User +/// 监听:DB 变化 → stream → Domain User → UI +/// ``` +abstract class UserRepository { + // ── 监听 ───────────────────────────────────────────────────────────────── + + /// 监听单个用户,DB 变化自动反映 + Stream watchUser(int uid); + + /// 监听指定 uid 列表,任一变化自动反映 + Stream> watchUsers(List uids); + + /// 监听所有用户,任一变化自动反映 + Stream> watchAllUsers(); + + // ── 读取 ───────────────────────────────────────────────────────────────── + + /// 从 DB 读取单个用户,不存在返回 null + Future getUser(int uid); + + /// 从 DB 读取所有用户 + Future> getAllUsers(); + + /// 分页读取用户 + /// + /// [offset] 起始偏移量 + /// [limit] 每页数量 + /// + /// 示例: + /// ```dart + /// // 第一页 + /// final page1 = await repo.getUsers(offset: 0, limit: 50); + /// // 第二页 + /// final page2 = await repo.getUsers(offset: 50, limit: 50); + /// ``` + Future> getUsers({required int offset, required int limit}); + + /// 统计 DB 中用户总数 + Future countUsers(); + + // ── 写入 ───────────────────────────────────────────────────────────────── + + /// 保存完整用户(insert or replace) + /// 调用方决定是否持久化 + Future saveUser(User user); + + /// 批量保存用户(insert or replace) + Future saveUsers(List users); + + /// 仅更新指定字段,不影响其他列 + /// + /// 示例: + /// ```dart + /// await repo.updateFields(uid, UsersCompanion( + /// nickname: Value('New Name'), + /// lastOnline: Value(DateTime.now().millisecondsSinceEpoch), + /// )); + /// ``` + Future updateFields(int uid, UsersCompanion companion); + + // ── 删除 ───────────────────────────────────────────────────────────────── + + /// 删除指定用户 + Future deleteUser(int uid); +} 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 00df1da..d012867 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 @@ -1,118 +1,154 @@ import 'dart:math'; -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import 'package:im_app/app/di/db_provider.dart'; -import 'package:im_app/data/local/drift/app_database.dart'; +import 'package:im_app/domain/entities/user.dart'; +import 'package:im_app/domain/presentation/di/user_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'chat_db_test_view_model.g.dart'; -class TestResult { - final String title; - final String subtitle; - final String duration; - - TestResult({ - required this.title, - required this.subtitle, - required this.duration, - }); -} - class ChatDbTestState { final bool testStarted; - final List testResults; + final List users; final String currentState; + final bool hasMore; + final int currentPage; + final int totalCount; const ChatDbTestState({ this.testStarted = false, - this.testResults = const [], + this.users = const [], this.currentState = '', + this.hasMore = true, + this.currentPage = 0, + this.totalCount = 0, }); ChatDbTestState copyWith({ bool? testStarted, - List? testResults, + List? users, String? currentState, + bool? hasMore, + int? currentPage, + int? totalCount, }) => ChatDbTestState( testStarted: testStarted ?? this.testStarted, - testResults: testResults ?? this.testResults, + users: users ?? this.users, currentState: currentState ?? this.currentState, + hasMore: hasMore ?? this.hasMore, + currentPage: currentPage ?? this.currentPage, + totalCount: totalCount ?? this.totalCount, ); } @riverpod class ChatDbTestViewModel extends _$ChatDbTestViewModel { + final _random = Random(); + bool _isTesting = false; + static const _pageSize = 50; @override ChatDbTestState build() { - // 这里就是 onInit - final List testResults = List.generate( - 1000, - (i) => TestResult( - title: '用户 ${Random().nextInt(9999)}', - subtitle: 'uid: ${Random().nextInt(999999)}', - duration: '${Random().nextInt(500)}ms', - ), - ); - return ChatDbTestState(testResults: testResults); + Future.microtask(() => _loadNextPage(reset: true)); + return const ChatDbTestState(currentState: '加载中...'); } - // ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ────────────────────────── + // ── 分页 ────────────────────────────────────────────────────────────────── + + Future _loadNextPage({bool reset = false}) async { + if (!state.hasMore && !reset) return; + + final repo = ref.read(userRepositoryProvider); + final page = reset ? 0 : state.currentPage; + final offset = page * _pageSize; + + final results = await Future.wait([ + repo.getUsers(offset: offset, limit: _pageSize), + repo.countUsers(), + ]); + + if (!ref.mounted) return; + + final newUsers = results[0] as List; + final total = results[1] as int; + + state = state.copyWith( + users: reset ? newUsers : [...state.users, ...newUsers], + currentPage: page + 1, + hasMore: newUsers.length >= _pageSize, + totalCount: total, + currentState: reset && newUsers.isEmpty ? '暂无数据' : '空闲 (共 $total 条)', + ); + } + + /// Called by ListView when reaching the end + void loadMore() { + if (!state.hasMore || state.testStarted) return; + _loadNextPage(); + } + + // ── 测试 ────────────────────────────────────────────────────────────────── - /// 开始测试 void startDBTest(BuildContext context) { + _isTesting = true; state = state.copyWith(testStarted: true, currentState: '开始测试'); _testDBInsert(); } - /// 结束测试 void stopDBTest(BuildContext context) { + _isTesting = false; state = state.copyWith(testStarted: false, currentState: '结束测试'); } Future _testDBInsert() async { - final db = ref.read(storageSdkProvider); + final repo = ref.read(userRepositoryProvider); const count = 10000; - const chunkSize = 50; + const chunkSize = 200; final stopwatch = Stopwatch()..start(); debugPrint('开始测试: $count 条,每批 $chunkSize 条'); + final workingList = List.from(state.users); int completed = 0; - for (var i = 0; i < count; i += chunkSize) { + while (completed < count) { + if (!_isTesting) break; + final chunk = List.generate( - chunkSize.clamp(0, count - i), - (j) => UsersCompanion.insert( - uid: i + j, - nickname: Value('User ${i + j}'), + min(chunkSize, count - completed), + (_) => User( + uid: _random.nextInt(999999), + nickname: 'User ${_random.nextInt(9999)}', ), ); - await db.batchInsertOrReplace(chunk); + await repo.saveUsers(chunk); completed += chunk.length; + workingList.addAll(chunk); - // 让出主线程 - await Future.delayed(Duration.zero); + debugPrint( + '已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)', + ); - debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)'); - - // 更新 UI 状态 if (ref.mounted) { state = state.copyWith( + users: List.unmodifiable(workingList), currentState: '已插入 $completed / $count 条', ); } } - debugPrint('全部完成: ${stopwatch.elapsedMilliseconds}ms'); + _isTesting = false; + final elapsed = stopwatch.elapsedMilliseconds; + debugPrint('全部完成: ${elapsed}ms'); + if (ref.mounted) { + final total = await repo.countUsers(); state = state.copyWith( testStarted: false, - currentState: '完成!共 $count 条,耗时 ${stopwatch.elapsedMilliseconds}ms', + totalCount: total, + currentState: '完成!共 $total 条,耗时 ${elapsed}ms', ); } } -} \ No newline at end of file +} 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..fe3f90f 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,69 +3,111 @@ 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 按钮) -/// -/// 包含五个演示按钮,覆盖 go_router 的常见导航场景: -/// - 「切换 Tab」 — go,替换历史,不可返回 -/// - 「有参 push(extra)」 — push + extra(Dart Record),可返回 -/// - 「有参 push(路径参数)」— push + URL 内嵌 id,可返回 -/// - 「无参 push」 — push,可返回 -/// - 「退出登录」 — 守卫自动重定向到 /login -/// -/// 所有操作通过 [ChatViewModel] 处理,View 不直接调用路由。 -/// 正式开发后替换为会话列表,按钮相关代码一并清除。 -class ChatDbTestPage extends ConsumerWidget { +class ChatDbTestPage extends ConsumerStatefulWidget { const ChatDbTestPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _ChatDbTestPageState(); +} + +class _ChatDbTestPageState extends ConsumerState { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 300) { + ref.read(chatDbTestViewModelProvider.notifier).loadMore(); + } + } + + @override + Widget build(BuildContext context) { final vm = ref.read(chatDbTestViewModelProvider.notifier); - final state = ref.watch(chatDbTestViewModelProvider); + final testStarted = ref.watch( + chatDbTestViewModelProvider.select((s) => s.testStarted), + ); + final currentState = ref.watch( + chatDbTestViewModelProvider.select((s) => s.currentState), + ); + final users = ref.watch(chatDbTestViewModelProvider.select((s) => s.users)); + final hasMore = ref.watch( + chatDbTestViewModelProvider.select((s) => s.hasMore), + ); + final totalCount = ref.watch( + chatDbTestViewModelProvider.select((s) => s.totalCount), + ); return Scaffold( - appBar: AppBar(title: const Text('测试数据库'), ), + appBar: AppBar(title: Text('测试数据库 ($totalCount)')), body: Column( mainAxisSize: MainAxisSize.max, spacing: 16, children: [ - SizedBox(height: 4), + const SizedBox(height: 4), Padding( - padding: EdgeInsetsGeometry.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppButton.inverse( - label: state.testStarted ? '结束' : '开始', - onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context), + label: testStarted ? '结束' : '开始', + onPressed: () => testStarted + ? vm.stopDBTest(context) + : vm.startDBTest(context), ), - SizedBox(width: 8), - Expanded( - child: Text( - state.currentState, - textAlign: TextAlign.end, - ), - ) + const SizedBox(width: 8), + Expanded(child: Text(currentState, textAlign: TextAlign.end)), ], - ) + ), ), Expanded( child: ListView.builder( - itemCount: state.testResults.length, + controller: _scrollController, + itemCount: users.length + (hasMore ? 1 : 0), + cacheExtent: 500, itemBuilder: (context, index) { - final result = state.testResults[index]; - return ListTile( - titleAlignment: ListTileTitleAlignment.center, - title: Text(result.title), - subtitle: Text(result.subtitle), - // trailing: Text(result.duration), - ); + if (index == users.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ); + } + final user = users[index]; + return _UserTile(uid: user.uid, nickname: user.nickname); }, ), - ) + ), ], ), ); } } + +class _UserTile extends StatelessWidget { + final int uid; + final String? nickname; + + const _UserTile({required this.uid, required this.nickname}); + + @override + Widget build(BuildContext context) { + return ListTile( + titleAlignment: ListTileTitleAlignment.center, + title: Text(nickname ?? '-'), + subtitle: Text('uid: $uid'), + ); + } +} 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 e81cd20..46689f1 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,8 +1,8 @@ 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:im_app/domain/presentation/di/user_provider.dart'; import 'package:networks_sdk/networks_sdk.dart'; import 'package:im_app/app/di/db_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -61,22 +61,25 @@ class LoginViewModel extends _$LoginViewModel { Future demoLogin() async { final storageApi = ref.read(storageSdkProvider); final storageLifeCycle = storageApi as StorageSdkLifecycle; + final repositoryProvider = ref.read(userRepositoryProvider); final provider = ref.read(authNotifierProvider); // Read mock response from assets final String raw = await rootBundle.loadString('assets/loginData.json'); final Map json = jsonDecode(raw); - // Parse into LoginData (nested under 'data' key) + // Parse → Domain User directly final loginResponse = LoginResponse.fromJson(json); final user = loginResponse.data.toEntity(); - provider.login(); // Open database for the user await storageLifeCycle.openDatabase(user.uid); - ///TODO: User 和 DTO和数据库之间转换 - final userCompanion = UserDto.fromEntity(user).toCompanion(); - storageApi.insert(userCompanion); + + // Save user to DB via repository + await repositoryProvider.saveUser(user); + + // Trigger auth state + provider.login(); } /// 执行登录 @@ -88,10 +91,9 @@ class LoginViewModel extends _$LoginViewModel { state = state.copyWith(isLoading: true, error: null); try { - final user = await ref.read(loginUseCaseProvider).execute( - email: email, - password: password, - ); + final user = await ref + .read(loginUseCaseProvider) + .execute(email: email, password: password); state = state.copyWith(user: user, isLoading: false); } on FormatException catch (e) { From fe54f79b21e866ec379969216a01050e8c8e9c48 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 14:59:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=A1=88=E4=BE=8B=E6=8C=89=E7=85=A7=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=9D=A5=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../di/user_provider.dart | 15 ++++- .../notifiers/user_notifier.dart | 2 +- .../usecases/insert_users_use_case.dart | 55 +++++++++++++++++++ .../presentation/chat_db_test_view_model.dart | 50 ++++++++--------- .../chat/usecases/insert_users_use_case.dart | 55 +++++++++++++++++++ .../login/presentation/login_view_model.dart | 2 +- 6 files changed, 147 insertions(+), 32 deletions(-) rename apps/im_app/lib/{domain/presentation => app}/di/user_provider.dart (64%) rename apps/im_app/lib/{domain/presentation => app}/notifiers/user_notifier.dart (97%) create mode 100644 apps/im_app/lib/domain/usecases/insert_users_use_case.dart create mode 100644 apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart diff --git a/apps/im_app/lib/domain/presentation/di/user_provider.dart b/apps/im_app/lib/app/di/user_provider.dart similarity index 64% rename from apps/im_app/lib/domain/presentation/di/user_provider.dart rename to apps/im_app/lib/app/di/user_provider.dart index b9a823a..9d6b95c 100644 --- a/apps/im_app/lib/domain/presentation/di/user_provider.dart +++ b/apps/im_app/lib/app/di/user_provider.dart @@ -1,3 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:im_app/domain/usecases/insert_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'; @@ -13,15 +15,22 @@ UserRepository userRepository(Ref ref) { return UserRepositoryImpl(ref.watch(storageSdkProvider)); } -// ── Multiple Users ──────────────────────────────────────────────────────────── +// ── Use Cases ───────────────────────────────────────────────────────────────── + +/// 批量插入用户用例 Provider +/// +/// 封装去重 + 分批插入逻辑,ViewModel 只需传入用户列表 +final insertUsersUseCaseProvider = Provider((ref) { + return InsertUsersUseCase(userRepository: ref.read(userRepositoryProvider)); +}); + +// ── Streams ─────────────────────────────────────────────────────────────────── @riverpod Stream> users(Ref ref, Set uids) { return ref.watch(userRepositoryProvider).watchUsers(uids.toList()); } -// ── All Users ───────────────────────────────────────────────────────────────── - @riverpod Stream> allUsers(Ref ref) { return ref.watch(userRepositoryProvider).watchAllUsers(); diff --git a/apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart b/apps/im_app/lib/app/notifiers/user_notifier.dart similarity index 97% rename from apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart rename to apps/im_app/lib/app/notifiers/user_notifier.dart index 2d2321a..56ce957 100644 --- a/apps/im_app/lib/domain/presentation/notifiers/user_notifier.dart +++ b/apps/im_app/lib/app/notifiers/user_notifier.dart @@ -1,4 +1,4 @@ -import 'package:im_app/domain/presentation/di/user_provider.dart'; +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'; 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 new file mode 100644 index 0000000..7223b1b --- /dev/null +++ b/apps/im_app/lib/domain/usecases/insert_users_use_case.dart @@ -0,0 +1,55 @@ +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/presentation/chat_db_test_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart index d012867..e9a60e9 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 @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:im_app/app/di/user_provider.dart'; import 'package:im_app/domain/entities/user.dart'; -import 'package:im_app/domain/presentation/di/user_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'chat_db_test_view_model.g.dart'; @@ -101,42 +101,38 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel { } Future _testDBInsert() async { + final useCase = ref.read(insertUsersUseCaseProvider); final repo = ref.read(userRepositoryProvider); const count = 10000; - const chunkSize = 200; final stopwatch = Stopwatch()..start(); + final baseUid = DateTime.now().microsecondsSinceEpoch; - debugPrint('开始测试: $count 条,每批 $chunkSize 条'); + debugPrint('开始测试: $count 条'); final workingList = List.from(state.users); - int completed = 0; - while (completed < count) { - if (!_isTesting) break; + final allUsers = List.generate( + count, + (i) => User(uid: baseUid + i, nickname: 'User ${_random.nextInt(9999)}'), + ); - final chunk = List.generate( - min(chunkSize, count - completed), - (_) => User( - uid: _random.nextInt(999999), - nickname: 'User ${_random.nextInt(9999)}', - ), - ); + await useCase.execute( + allUsers, + onProgress: (completed, total, chunk) { + workingList.addAll(chunk); - await repo.saveUsers(chunk); - completed += chunk.length; - workingList.addAll(chunk); - - debugPrint( - '已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)', - ); - - if (ref.mounted) { - state = state.copyWith( - users: List.unmodifiable(workingList), - currentState: '已插入 $completed / $count 条', + debugPrint( + '已完成: $completed / $total (${stopwatch.elapsedMilliseconds}ms)', ); - } - } + + if (ref.mounted && _isTesting) { + state = state.copyWith( + users: List.unmodifiable(workingList), + currentState: '已插入 $completed / $total 条', + ); + } + }, + ); _isTesting = false; final elapsed = stopwatch.elapsedMilliseconds; 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 new file mode 100644 index 0000000..7223b1b --- /dev/null +++ b/apps/im_app/lib/features/chat/usecases/insert_users_use_case.dart @@ -0,0 +1,55 @@ +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/login/presentation/login_view_model.dart b/apps/im_app/lib/features/login/presentation/login_view_model.dart index 46689f1..83b7012 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,8 +1,8 @@ import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:im_app/app/di/user_provider.dart'; import 'package:im_app/data/remote/login_request.dart'; -import 'package:im_app/domain/presentation/di/user_provider.dart'; import 'package:networks_sdk/networks_sdk.dart'; import 'package:im_app/app/di/db_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';