Merge pull request '业务更新User所需(企业成员、聊天室群组成员)' (#10) from happi/dev/database-update into dev
Reviewed-on: https://gitea.winwayinfo.com/CUS-IM/customer-im-client/pulls/10
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:im_app/app/di/db_provider.dart';
|
import 'package:im_app/app/di/db_provider.dart';
|
||||||
import 'package:im_app/data/repositories/user_repository_impl.dart';
|
import 'package:im_app/data/repositories/user_repository_impl.dart';
|
||||||
@@ -24,6 +26,20 @@ final insertUsersUseCaseProvider = Provider<InsertUsersUseCase>((ref) {
|
|||||||
return InsertUsersUseCase(userRepository: ref.read(userRepositoryProvider));
|
return InsertUsersUseCase(userRepository: ref.read(userRepositoryProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 批量更新用户用例 Provider
|
||||||
|
///
|
||||||
|
/// 取前10条,随机生成昵称,批量更新到 DB
|
||||||
|
final updateUsersUseCaseProvider = Provider<UpdateUsersUseCase>((ref) {
|
||||||
|
return UpdateUsersUseCase(userRepository: ref.read(userRepositoryProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 删除前10个用户用例 Provider
|
||||||
|
///
|
||||||
|
/// 取前10条,按 uid 逐条删除
|
||||||
|
final deleteUsersUseCaseProvider = Provider<DeleteUsersUseCase>((ref) {
|
||||||
|
return DeleteUsersUseCase(userRepository: ref.read(userRepositoryProvider));
|
||||||
|
});
|
||||||
|
|
||||||
// ── Streams ───────────────────────────────────────────────────────────────────
|
// ── Streams ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:im_app/app/di/user_provider.dart';
|
import 'package:im_app/app/di/user_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.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/entities/user.dart';
|
||||||
import 'package:im_app/domain/repositories/user_repository.dart';
|
import 'package:im_app/domain/repositories/user_repository.dart';
|
||||||
|
|
||||||
@@ -16,10 +15,16 @@ part 'user_notifier.g.dart';
|
|||||||
/// // 即时读取,无需 await
|
/// // 即时读取,无需 await
|
||||||
/// final user = ref.read(userNotifierProvider(123).notifier).current;
|
/// final user = ref.read(userNotifierProvider(123).notifier).current;
|
||||||
///
|
///
|
||||||
/// // 部分更新
|
/// // 插入或替换
|
||||||
/// ref.read(userNotifierProvider(123).notifier).updateFields(
|
/// ref.read(userNotifierProvider(123).notifier).insertOrReplaceUser(user);
|
||||||
/// UsersCompanion(nickname: Value('New Name')),
|
///
|
||||||
|
/// // 单个更新
|
||||||
|
/// ref.read(userNotifierProvider(123).notifier).updateUser(
|
||||||
|
/// user.copyWith(nickname: 'New Name'),
|
||||||
/// );
|
/// );
|
||||||
|
///
|
||||||
|
/// // 批量更新
|
||||||
|
/// ref.read(userNotifierProvider(123).notifier).updateUsers(updatedList);
|
||||||
/// ```
|
/// ```
|
||||||
@riverpod
|
@riverpod
|
||||||
class UserNotifier extends _$UserNotifier {
|
class UserNotifier extends _$UserNotifier {
|
||||||
@@ -31,13 +36,11 @@ class UserNotifier extends _$UserNotifier {
|
|||||||
Future<User?> build(int uid) async {
|
Future<User?> build(int uid) async {
|
||||||
ref.onDispose(() => _cached = null);
|
ref.onDispose(() => _cached = null);
|
||||||
|
|
||||||
// Stream starts automatically — uid is the family arg
|
|
||||||
_repo.watchUser(uid).listen((user) {
|
_repo.watchUser(uid).listen((user) {
|
||||||
_cached = user;
|
_cached = user;
|
||||||
state = AsyncData(user);
|
state = AsyncData(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return initial DB value
|
|
||||||
return _repo.getUser(uid);
|
return _repo.getUser(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,16 +50,31 @@ class UserNotifier extends _$UserNotifier {
|
|||||||
|
|
||||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Future<void> saveUser(User user) async {
|
/// 插入或替换单个用户
|
||||||
await _repo.saveUser(user);
|
Future<void> insertOrReplaceUser(User user) async {
|
||||||
}
|
await _repo.insertOrReplaceUser(user);
|
||||||
|
|
||||||
Future<void> updateFields(UsersCompanion companion) async {
|
|
||||||
await _repo.updateFields(uid, companion); // uid from build arg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新单个用户所有字段,按 uid 匹配
|
||||||
|
///
|
||||||
|
/// 示例:
|
||||||
|
/// ```dart
|
||||||
|
/// await notifier.updateUser(user.copyWith(nickname: 'New Name'));
|
||||||
|
/// ```
|
||||||
Future<void> updateUser(User user) async {
|
Future<void> 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<void> updateUsers(List<User> users) async {
|
||||||
|
await _repo.updateUsersBatch(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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<String, dynamic> json) => CallLogDto(
|
|
||||||
id: json['id'] as String,
|
|
||||||
callerId: json['caller_id'],
|
|
||||||
receiverId: json['receiver_id'],
|
|
||||||
chatId: json['chat_id'],
|
|
||||||
duration: json['duration'],
|
|
||||||
videoCall: json['video_call'],
|
|
||||||
createdAt: json['created_at'],
|
|
||||||
updatedAt: json['updated_at'],
|
|
||||||
endedAt: json['ended_at'],
|
|
||||||
status: json['status'],
|
|
||||||
isDeleted: json['is_deleted'],
|
|
||||||
deletedAt: json['deleted_at'],
|
|
||||||
isRead: json['is_read'],
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'id': id,
|
|
||||||
'caller_id': callerId,
|
|
||||||
'receiver_id': receiverId,
|
|
||||||
'chat_id': chatId,
|
|
||||||
'duration': duration,
|
|
||||||
'video_call': videoCall,
|
|
||||||
'created_at': createdAt,
|
|
||||||
'updated_at': updatedAt,
|
|
||||||
'ended_at': endedAt,
|
|
||||||
'status': status,
|
|
||||||
'is_deleted': isDeleted,
|
|
||||||
'deleted_at': deletedAt,
|
|
||||||
'is_read': isRead,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// DTO → Domain Entity
|
|
||||||
CallLog toEntity() => CallLog(
|
|
||||||
id: id,
|
|
||||||
callerId: callerId,
|
|
||||||
receiverId: receiverId,
|
|
||||||
chatId: chatId,
|
|
||||||
duration: duration,
|
|
||||||
videoCall: videoCall,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
endedAt: endedAt,
|
|
||||||
status: status,
|
|
||||||
isDeleted: isDeleted,
|
|
||||||
deletedAt: deletedAt,
|
|
||||||
isRead: isRead,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Domain Entity → DTO
|
|
||||||
factory CallLogDto.fromEntity(CallLog callLog) => CallLogDto(
|
|
||||||
id: callLog.id,
|
|
||||||
callerId: callLog.callerId,
|
|
||||||
receiverId: callLog.receiverId,
|
|
||||||
chatId: callLog.chatId,
|
|
||||||
duration: callLog.duration,
|
|
||||||
videoCall: callLog.videoCall,
|
|
||||||
createdAt: callLog.createdAt,
|
|
||||||
updatedAt: callLog.updatedAt,
|
|
||||||
endedAt: callLog.endedAt,
|
|
||||||
status: callLog.status,
|
|
||||||
isDeleted: callLog.isDeleted,
|
|
||||||
deletedAt: callLog.deletedAt,
|
|
||||||
isRead: callLog.isRead,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// DTO → Drift Companion (for DB insert/update)
|
|
||||||
CallLogsCompanion toCompanion() => CallLogsCompanion(
|
|
||||||
id: Value(id),
|
|
||||||
callerId: Value(callerId),
|
|
||||||
receiverId: Value(receiverId),
|
|
||||||
chatId: Value(chatId),
|
|
||||||
duration: Value(duration),
|
|
||||||
videoCall: Value(videoCall),
|
|
||||||
createdAt: Value(createdAt),
|
|
||||||
updatedAt: Value(updatedAt),
|
|
||||||
endedAt: Value(endedAt),
|
|
||||||
status: Value(status),
|
|
||||||
isDeleted: Value(isDeleted),
|
|
||||||
deletedAt: Value(deletedAt),
|
|
||||||
isRead: Value(isRead),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
165
apps/im_app/lib/data/repositories/call_log_repository_impl.dart
Normal file
165
apps/im_app/lib/data/repositories/call_log_repository_impl.dart
Normal file
@@ -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<List<CallLog>> watchAllCallLogs() {
|
||||||
|
return _storage.watchAll<DriftCallLog>().map(
|
||||||
|
(rows) => rows.map(_toEntity).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CallLog?> watchCallLog(String id) {
|
||||||
|
return _storage
|
||||||
|
.watchFirst<DriftCallLog, $CallLogsTable>((t) => t.id.equals(id))
|
||||||
|
.map((row) => row != null ? _toEntity(row) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<CallLog>> getCallLogs() async {
|
||||||
|
final rows = await _storage.rawQuery(
|
||||||
|
'SELECT * FROM call_log ORDER BY updated_at DESC',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return rows
|
||||||
|
.map(
|
||||||
|
(row) => CallLog(
|
||||||
|
id: row.read<String>('id'),
|
||||||
|
callerId: row.readNullable<int>('caller_id'),
|
||||||
|
receiverId: row.readNullable<int>('receiver_id'),
|
||||||
|
chatId: row.readNullable<int>('chat_id'),
|
||||||
|
duration: row.readNullable<int>('duration'),
|
||||||
|
videoCall: row.readNullable<int>('video_call'),
|
||||||
|
createdAt: row.readNullable<int>('created_at'),
|
||||||
|
updatedAt: row.readNullable<int>('updated_at'),
|
||||||
|
endedAt: row.readNullable<int>('ended_at'),
|
||||||
|
status: row.readNullable<int>('status'),
|
||||||
|
isDeleted: row.readNullable<int>('is_deleted'),
|
||||||
|
deletedAt: row.readNullable<int>('deleted_at'),
|
||||||
|
isRead: row.readNullable<int>('is_read'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CallLog?> getCallLog(String id) async {
|
||||||
|
final row = await _storage.selectFirst<DriftCallLog, $CallLogsTable>(
|
||||||
|
(t) => t.id.equals(id),
|
||||||
|
);
|
||||||
|
return row != null ? _toEntity(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isExist(String id) async {
|
||||||
|
final row = await _storage.selectFirst<DriftCallLog, $CallLogsTable>(
|
||||||
|
(t) => t.id.equals(id),
|
||||||
|
);
|
||||||
|
return row != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> 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<int>('count');
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insertOrReplaceCallLog(CallLog callLog) async {
|
||||||
|
await _storage.insertOrReplace<DriftCallLog>(_toCompanion(callLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insertOrReplaceCallLogs(List<CallLog> callLogs) async {
|
||||||
|
await _storage.batchInsertOrReplace<DriftCallLog>(
|
||||||
|
callLogs.map(_toCompanion).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> markAllAsRead() async {
|
||||||
|
await _storage.rawQuery('UPDATE call_log SET is_read = 1', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteCallLog(String id) async {
|
||||||
|
await _storage.deleteWhere<DriftCallLog, $CallLogsTable>(
|
||||||
|
(t) => t.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
179
apps/im_app/lib/data/repositories/chat_bot_repository_impl.dart
Normal file
179
apps/im_app/lib/data/repositories/chat_bot_repository_impl.dart
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/app_database.dart';
|
||||||
|
import 'package:im_app/domain/entities/chat_bot.dart';
|
||||||
|
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<List<ChatBot>> watchAllChatBots() {
|
||||||
|
return _storage.watchAll<DriftChatBot>().map(
|
||||||
|
(rows) => rows.map(_toEntity).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<ChatBot?> watchChatBot(int id) {
|
||||||
|
return _storage
|
||||||
|
.watchFirst<DriftChatBot, $ChatBotsTable>((t) => t.id.equals(id))
|
||||||
|
.map((row) => row != null ? _toEntity(row) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ChatBot>> 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<int>('id'),
|
||||||
|
name: row.readNullable<String>('name'),
|
||||||
|
username: row.readNullable<String>('username'),
|
||||||
|
botUserId: row.readNullable<int>('bot_user_id'),
|
||||||
|
icon: row.readNullable<String>('icon'),
|
||||||
|
iconGaussian: row.readNullable<String>('icon_gaussian'),
|
||||||
|
description: row.readNullable<String>('description'),
|
||||||
|
token: row.readNullable<String>('token'),
|
||||||
|
flag: row.readNullable<int>('flag'),
|
||||||
|
status: row.readNullable<int>('status'),
|
||||||
|
webhook: row.readNullable<String>('webhook'),
|
||||||
|
commands: row.readNullable<String>('commands'),
|
||||||
|
banner: row.readNullable<String>('banner'),
|
||||||
|
channelId: row.readNullable<int>('channel_id'),
|
||||||
|
channelGroupId: row.readNullable<int>('channel_group_id'),
|
||||||
|
deletedAt: row.readNullable<int>('deleted_at'),
|
||||||
|
internalWebhook: row.readNullable<String>('internal_webhook'),
|
||||||
|
mode: row.readNullable<int>('mode'),
|
||||||
|
redirectUrl: row.readNullable<String>('redirect_url'),
|
||||||
|
isInvitable: row.readNullable<int>('is_invitable'),
|
||||||
|
isAllowForward: row.readNullable<int>('is_allow_forward'),
|
||||||
|
tips: row.readNullable<String>('tips'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final rows = await _storage.selectAll<DriftChatBot>();
|
||||||
|
return rows.map(_toEntity).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ChatBot?> getChatBot(int id) async {
|
||||||
|
final row = await _storage.selectFirst<DriftChatBot, $ChatBotsTable>(
|
||||||
|
(t) => t.id.equals(id),
|
||||||
|
);
|
||||||
|
return row != null ? _toEntity(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insertOrReplaceChatBot(ChatBot chatBot) async {
|
||||||
|
await _storage.insertOrReplace<DriftChatBot>(_toCompanion(chatBot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insertOrReplaceChatBots(List<ChatBot> chatBots) async {
|
||||||
|
await _storage.batchInsertOrReplace<DriftChatBot>(
|
||||||
|
chatBots.map(_toCompanion).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateChatBot(ChatBot chatBot) async {
|
||||||
|
await _storage.updateWhere<DriftChatBot, $ChatBotsTable>(
|
||||||
|
_toCompanion(chatBot),
|
||||||
|
(t) => t.id.equals(chatBot.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteChatBot(int id) async {
|
||||||
|
await _storage.deleteWhere<DriftChatBot, $ChatBotsTable>(
|
||||||
|
(t) => t.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearChatBots() async {
|
||||||
|
await _storage.deleteAll<DriftChatBot>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,13 @@ import 'package:storage_sdk/storage_sdk.dart';
|
|||||||
/// ## 职责
|
/// ## 职责
|
||||||
/// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase
|
/// - 所有 DB 操作通过 [StorageSdkApi],不直接接触 AppDatabase
|
||||||
/// - DriftUser ↔ Domain User 映射
|
/// - 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
|
/// 监听:StorageSdkApi.watchWhere/watchAll → DriftUser → _toEntity() → UI
|
||||||
/// 部分更新:updateFields(uid, UsersCompanion) → StorageSdkApi.updateWhere
|
/// 更新:updateUser(user) / updateUsersBatch(users) → _toCompanion() → DB
|
||||||
/// ```
|
/// ```
|
||||||
class UserRepositoryImpl implements UserRepository {
|
class UserRepositoryImpl implements UserRepository {
|
||||||
final StorageSdkApi _storage;
|
final StorageSdkApi _storage;
|
||||||
@@ -60,7 +60,7 @@ class UserRepositoryImpl implements UserRepository {
|
|||||||
hint: row.hint,
|
hint: row.hint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Domain → DB companion ────────────────────────────────────────────────
|
// ── Domain → DB companion (internal only) ────────────────────────────────
|
||||||
|
|
||||||
UsersCompanion _toCompanion(User user) => UsersCompanion(
|
UsersCompanion _toCompanion(User user) => UsersCompanion(
|
||||||
uid: Value(user.uid),
|
uid: Value(user.uid),
|
||||||
@@ -98,9 +98,92 @@ class UserRepositoryImpl implements UserRepository {
|
|||||||
hint: Value(user.hint),
|
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
|
@override
|
||||||
Stream<User?> watchUser(int uid) {
|
Stream<User?> watchUser(int uid) {
|
||||||
return _storage
|
return _storage
|
||||||
@@ -108,7 +191,6 @@ class UserRepositoryImpl implements UserRepository {
|
|||||||
.map((row) => row != null ? _toEntity(row) : null);
|
.map((row) => row != null ? _toEntity(row) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 监听指定 uid 列表
|
|
||||||
@override
|
@override
|
||||||
Stream<List<User>> watchUsers(List<int> uids) {
|
Stream<List<User>> watchUsers(List<int> uids) {
|
||||||
return _storage
|
return _storage
|
||||||
@@ -116,7 +198,6 @@ class UserRepositoryImpl implements UserRepository {
|
|||||||
.map((rows) => rows.map(_toEntity).toList());
|
.map((rows) => rows.map(_toEntity).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 监听所有用户
|
|
||||||
@override
|
@override
|
||||||
Stream<List<User>> watchAllUsers() {
|
Stream<List<User>> watchAllUsers() {
|
||||||
return _storage.watchAll<DriftUser>().map(
|
return _storage.watchAll<DriftUser>().map(
|
||||||
@@ -199,34 +280,80 @@ class UserRepositoryImpl implements UserRepository {
|
|||||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveUser(User user) async {
|
Future<void> insertOrReplaceUser(User user) async {
|
||||||
await _storage.insertOrReplace<DriftUser>(_toCompanion(user));
|
if (user.requireUpsert) {
|
||||||
|
await _storage.insertOrReplace<DriftUser>(_toCompanion(user));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveUsers(List<User> users) async {
|
Future<void> insertOrReplaceUsers(List<User> users) async {
|
||||||
await _storage.batchInsertOrReplace<DriftUser>(
|
final List<User> upsertList = [];
|
||||||
users.map(_toCompanion).toList(),
|
final List<User> insertList = [];
|
||||||
);
|
for (final user in users) {
|
||||||
|
user.requireUpsert ? upsertList.add(user) : insertList.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertList.isNotEmpty) {
|
||||||
|
await _storage.batchInsertOrReplace<DriftUser>(
|
||||||
|
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
|
@override
|
||||||
Future<void> updateFields(int uid, UsersCompanion companion) async {
|
Future<void> updateUser(User user) async {
|
||||||
await _storage.updateWhere<DriftUser, $UsersTable>(
|
await _storage.updateWhere<DriftUser, $UsersTable>(
|
||||||
companion,
|
_toCompanion(user),
|
||||||
(t) => t.uid.equals(uid),
|
(t) => t.uid.equals(user.uid),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量更新已存在的用户记录。
|
||||||
|
///
|
||||||
|
/// 仅更新,不插入——调用方应确保这些用户已存在于 DB 中。
|
||||||
|
/// 如需 upsert 语义,请使用 [upsertUsers]。
|
||||||
|
///
|
||||||
|
/// 所有更新在同一事务内完成,保证原子性。
|
||||||
|
@override
|
||||||
|
Future<void> updateUsersBatch(List<User> users) async {
|
||||||
|
await _storage.transaction(() async {
|
||||||
|
for (final user in users) {
|
||||||
|
await updateUser(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单条 upsert:不存在则插入,存在则仅更新非 null 字段。
|
||||||
|
///
|
||||||
|
/// 内部委托给 [upsertUsers]。
|
||||||
|
@override
|
||||||
|
Future<void> upsertUser(User user) => upsertUsers([user]);
|
||||||
|
|
||||||
|
/// 批量 upsert:不存在则插入,存在则仅更新非 null 字段。
|
||||||
|
///
|
||||||
|
/// 与 [insertOrReplaceUsers] 的区别:
|
||||||
|
/// - [insertOrReplaceUsers] → INSERT OR REPLACE,全字段覆盖
|
||||||
|
/// - [upsertUsers] → INSERT OR IGNORE + UPDATE,仅更新传入的非 null 字段
|
||||||
|
///
|
||||||
|
/// 所有操作在同一事务内完成,保证原子性。
|
||||||
|
/// 适用场景:从服务端收到部分用户信息,不希望覆盖本地已有的其他字段。
|
||||||
|
@override
|
||||||
|
Future<void> upsertUsers(List<User> users) async {
|
||||||
|
await _storage.transaction(() async {
|
||||||
|
for (final user in users) {
|
||||||
|
await _storage.insert<DriftUser>(_toCompanion(user));
|
||||||
|
await _storage.updateWhere<DriftUser, $UsersTable>(
|
||||||
|
_toPartialCompanion(user),
|
||||||
|
(t) => t.uid.equals(user.uid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
24
apps/im_app/lib/domain/entities/company_member.dart
Normal file
24
apps/im_app/lib/domain/entities/company_member.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:im_app/domain/entities/user.dart';
|
||||||
|
|
||||||
|
/// 企业成员的部分 [User] 表示,数据来源于企业成员接口。
|
||||||
|
///
|
||||||
|
/// 企业 API 返回的用户信息极为精简,仅包含身份、显示名称和在线状态。
|
||||||
|
/// 如需完整用户信息,请通过用户详情接口获取完整的 [User]。
|
||||||
|
///
|
||||||
|
/// 注意企业 API 的字段名与标准用户接口不同:
|
||||||
|
/// - `user_id` → [uid] (非 `uid`)
|
||||||
|
/// - `name` → [nickname] (非 `nickname`)
|
||||||
|
class CompanyMember extends User {
|
||||||
|
const CompanyMember({
|
||||||
|
required super.uid,
|
||||||
|
super.nickname,
|
||||||
|
super.lastOnline,
|
||||||
|
super.requireUpsert = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CompanyMember.fromJson(Map<String, dynamic> json) => CompanyMember(
|
||||||
|
uid: json['user_id'] as int,
|
||||||
|
nickname: json['name'],
|
||||||
|
lastOnline: json['last_online'],
|
||||||
|
);
|
||||||
|
}
|
||||||
66
apps/im_app/lib/domain/entities/group_member.dart
Normal file
66
apps/im_app/lib/domain/entities/group_member.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:im_app/domain/entities/user.dart';
|
||||||
|
|
||||||
|
/// 群组成员的部分 [User] 表示,数据来源于群组成员接口。
|
||||||
|
///
|
||||||
|
/// 群组 API 返回的用户信息不完整,仅保证身份和在线状态字段有效。
|
||||||
|
/// 构造函数中未列出的 [User] 字段均为 null,不应依赖这些字段。
|
||||||
|
///
|
||||||
|
/// ## 何时使用 [GroupMember] vs [User]
|
||||||
|
/// - 渲染群组成员列表、成员头像等场景使用 [GroupMember]
|
||||||
|
/// - 需要联系方式、音效设置、好友关系等完整信息时,
|
||||||
|
/// 请通过用户详情接口获取完整的 [User]
|
||||||
|
///
|
||||||
|
/// ## 数据流
|
||||||
|
/// ```
|
||||||
|
/// 群组 API 响应
|
||||||
|
/// → GroupMember.fromJson() ← 群组专用字段(user_id、icon…)
|
||||||
|
/// → ★ GroupMember ★
|
||||||
|
/// → 仍是合法的 [User],可在任何需要 [User] 的地方使用
|
||||||
|
/// ```
|
||||||
|
class GroupMember extends User {
|
||||||
|
const GroupMember({
|
||||||
|
required super.uid,
|
||||||
|
super.nickname,
|
||||||
|
super.profilePic,
|
||||||
|
super.profilePicGaussian,
|
||||||
|
super.lastOnline,
|
||||||
|
super.deletedAt,
|
||||||
|
super.role,
|
||||||
|
super.requireUpsert = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 从群组成员 JSON 数据创建 [GroupMember]。
|
||||||
|
///
|
||||||
|
/// 注意群组 API 使用了与用户接口不同的字段名:
|
||||||
|
/// - `user_id` → [uid] (非 `uid`)
|
||||||
|
/// - `user_name` → [nickname] (非 `nickname`)
|
||||||
|
/// - `icon` → [profilePic] (非 `profile_pic`)
|
||||||
|
/// - `icon_gaussian` → [profilePicGaussian]
|
||||||
|
/// - `delete_time` → [deletedAt] (非 `deleted_at`)
|
||||||
|
factory GroupMember.fromJson(Map<String, dynamic> json) => GroupMember(
|
||||||
|
uid: json['user_id'] as int,
|
||||||
|
nickname: json['user_name'],
|
||||||
|
profilePic: json['icon'],
|
||||||
|
profilePicGaussian: json['icon_gaussian'],
|
||||||
|
lastOnline: json['last_online'],
|
||||||
|
deletedAt: json['delete_time'],
|
||||||
|
role: json['role'],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 序列化为群组成员 JSON 格式。
|
||||||
|
///
|
||||||
|
/// 字段名与 [fromJson] 保持对称。
|
||||||
|
/// `id`/`user_id`、`nickname`/`user_name`、`icon`/`profile_pic`
|
||||||
|
/// 同时输出两种命名,兼容不同约定的下游消费方。
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': uid,
|
||||||
|
'user_id': uid,
|
||||||
|
'user_name': nickname,
|
||||||
|
'nickname': nickname, // 兼容旧命名
|
||||||
|
'icon': profilePic,
|
||||||
|
'profile_pic': profilePic, // 兼容旧命名
|
||||||
|
'icon_gaussian': profilePicGaussian,
|
||||||
|
'last_online': lastOnline,
|
||||||
|
'delete_time': deletedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -47,6 +47,24 @@ class User {
|
|||||||
final int? configBits;
|
final int? configBits;
|
||||||
final String? hint;
|
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({
|
const User({
|
||||||
required this.uid,
|
required this.uid,
|
||||||
this.uuid,
|
this.uuid,
|
||||||
@@ -81,9 +99,13 @@ class User {
|
|||||||
this.publicKey,
|
this.publicKey,
|
||||||
this.configBits,
|
this.configBits,
|
||||||
this.hint,
|
this.hint,
|
||||||
|
this.requireUpsert = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 直接从网络 JSON 创建 Domain 实体
|
/// 直接从网络 JSON 创建 Domain 实体。
|
||||||
|
///
|
||||||
|
/// [requireUpsert] 默认 false,如响应解析层判断为部分数据,
|
||||||
|
/// 可在调用后通过 copyWith(requireUpsert: true) 标记。
|
||||||
factory User.fromJson(Map<String, dynamic> json) => User(
|
factory User.fromJson(Map<String, dynamic> json) => User(
|
||||||
uid: json['uid'] as int,
|
uid: json['uid'] as int,
|
||||||
uuid: json['uuid'],
|
uuid: json['uuid'],
|
||||||
@@ -120,7 +142,10 @@ class User {
|
|||||||
hint: json['hint'],
|
hint: json['hint'],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 仅更新部分字段
|
/// 仅更新部分字段,其余保持不变。
|
||||||
|
///
|
||||||
|
/// 注意:[requireUpsert] 不会随其他字段自动继承,
|
||||||
|
/// 需要显式传入以避免意外的写入策略变更。
|
||||||
User copyWith({
|
User copyWith({
|
||||||
int? uid,
|
int? uid,
|
||||||
String? uuid,
|
String? uuid,
|
||||||
@@ -155,6 +180,7 @@ class User {
|
|||||||
String? publicKey,
|
String? publicKey,
|
||||||
int? configBits,
|
int? configBits,
|
||||||
String? hint,
|
String? hint,
|
||||||
|
bool? requireUpsert,
|
||||||
}) {
|
}) {
|
||||||
return User(
|
return User(
|
||||||
uid: uid ?? this.uid,
|
uid: uid ?? this.uid,
|
||||||
@@ -191,6 +217,7 @@ class User {
|
|||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
configBits: configBits ?? this.configBits,
|
configBits: configBits ?? this.configBits,
|
||||||
hint: hint ?? this.hint,
|
hint: hint ?? this.hint,
|
||||||
|
requireUpsert: requireUpsert ?? this.requireUpsert,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
apps/im_app/lib/domain/repositories/call_log_repository.dart
Normal file
56
apps/im_app/lib/domain/repositories/call_log_repository.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:im_app/domain/entities/call_log.dart';
|
||||||
|
|
||||||
|
/// 通话记录仓储接口
|
||||||
|
///
|
||||||
|
/// ## 职责
|
||||||
|
/// - StorageSdkApi ↔ Domain CallLog 映射
|
||||||
|
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||||
|
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||||
|
///
|
||||||
|
/// ## 数据流
|
||||||
|
/// ```
|
||||||
|
/// 写入:Domain CallLog → _toCompanion() → StorageSdkApi → DB
|
||||||
|
/// 读取:DB row (DriftCallLog) → _toEntity() → Domain CallLog
|
||||||
|
/// 监听:DB 变化 → stream → Domain CallLog → UI
|
||||||
|
/// ```
|
||||||
|
abstract class CallLogRepository {
|
||||||
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 监听所有通话记录,DB 变化自动反映
|
||||||
|
Stream<List<CallLog>> watchAllCallLogs();
|
||||||
|
|
||||||
|
/// 监听指定通话记录
|
||||||
|
Stream<CallLog?> watchCallLog(String id);
|
||||||
|
|
||||||
|
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 读取所有通话记录,按 updated_at 降序
|
||||||
|
Future<List<CallLog>> getCallLogs();
|
||||||
|
|
||||||
|
/// 读取指定通话记录,不存在返回 null
|
||||||
|
Future<CallLog?> getCallLog(String id);
|
||||||
|
|
||||||
|
/// 检查通话记录是否存在
|
||||||
|
Future<bool> isExist(String id);
|
||||||
|
|
||||||
|
/// 获取未读通话数量
|
||||||
|
///
|
||||||
|
/// 统计 is_read = 0 且非主叫的未接/取消/结束/超时通话
|
||||||
|
Future<int> getUnreadCount(int currentUid);
|
||||||
|
|
||||||
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 插入或替换通话记录
|
||||||
|
Future<void> insertOrReplaceCallLog(CallLog callLog);
|
||||||
|
|
||||||
|
/// 批量插入或替换通话记录
|
||||||
|
Future<void> insertOrReplaceCallLogs(List<CallLog> callLogs);
|
||||||
|
|
||||||
|
/// 将所有通话记录标记为已读
|
||||||
|
Future<void> markAllAsRead();
|
||||||
|
|
||||||
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 删除指定通话记录
|
||||||
|
Future<void> deleteCallLog(String id);
|
||||||
|
}
|
||||||
53
apps/im_app/lib/domain/repositories/chat_bot_repository.dart
Normal file
53
apps/im_app/lib/domain/repositories/chat_bot_repository.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:im_app/domain/entities/chat_bot.dart';
|
||||||
|
|
||||||
|
/// 聊天机器人仓储接口
|
||||||
|
///
|
||||||
|
/// ## 职责
|
||||||
|
/// - StorageSdkApi ↔ Domain ChatBot 映射
|
||||||
|
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||||
|
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||||
|
///
|
||||||
|
/// ## 数据流
|
||||||
|
/// ```
|
||||||
|
/// 写入:Domain ChatBot → _toCompanion() → StorageSdkApi → DB
|
||||||
|
/// 读取:DB row (DriftChatBot) → _toEntity() → Domain ChatBot
|
||||||
|
/// 监听:DB 变化 → stream → Domain ChatBot → UI
|
||||||
|
/// ```
|
||||||
|
abstract class ChatBotRepository {
|
||||||
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 监听所有聊天机器人,DB 变化自动反映
|
||||||
|
Stream<List<ChatBot>> watchAllChatBots();
|
||||||
|
|
||||||
|
/// 监听指定聊天机器人
|
||||||
|
Stream<ChatBot?> watchChatBot(int id);
|
||||||
|
|
||||||
|
// ── 读取 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 读取所有聊天机器人
|
||||||
|
///
|
||||||
|
/// [limit] 可选限制数量
|
||||||
|
Future<List<ChatBot>> getChatBots({int? limit});
|
||||||
|
|
||||||
|
/// 读取指定聊天机器人,不存在返回 null
|
||||||
|
Future<ChatBot?> getChatBot(int id);
|
||||||
|
|
||||||
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 插入或替换聊天机器人
|
||||||
|
Future<void> insertOrReplaceChatBot(ChatBot chatBot);
|
||||||
|
|
||||||
|
/// 批量插入或替换聊天机器人
|
||||||
|
Future<void> insertOrReplaceChatBots(List<ChatBot> chatBots);
|
||||||
|
|
||||||
|
/// 更新聊天机器人
|
||||||
|
Future<void> updateChatBot(ChatBot chatBot);
|
||||||
|
|
||||||
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 删除指定聊天机器人
|
||||||
|
Future<void> deleteChatBot(int id);
|
||||||
|
|
||||||
|
/// 清空所有聊天机器人
|
||||||
|
Future<void> clearChatBots();
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:im_app/data/local/drift/app_database.dart';
|
|
||||||
import 'package:im_app/domain/entities/user.dart';
|
import 'package:im_app/domain/entities/user.dart';
|
||||||
|
|
||||||
/// 用户仓储接口
|
/// 用户仓储接口
|
||||||
@@ -7,6 +6,7 @@ import 'package:im_app/domain/entities/user.dart';
|
|||||||
/// - StorageSdkApi ↔ Domain User 映射
|
/// - StorageSdkApi ↔ Domain User 映射
|
||||||
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
/// - CRUD 操作(通过 StorageSdkApi,不直接接触 DB)
|
||||||
/// - 实时监听(单个 / 多个 / 全部)
|
/// - 实时监听(单个 / 多个 / 全部)
|
||||||
|
/// - 所有公开接口只接受 Domain 实体,Companion 转换完全内聚在 Impl
|
||||||
///
|
///
|
||||||
/// ## 数据流
|
/// ## 数据流
|
||||||
/// ```
|
/// ```
|
||||||
@@ -14,7 +14,7 @@ import 'package:im_app/domain/entities/user.dart';
|
|||||||
/// 读取:DB row (DriftUser) → _toEntity() → Domain User
|
/// 读取:DB row (DriftUser) → _toEntity() → Domain User
|
||||||
/// 监听:DB 变化 → stream → Domain User → UI
|
/// 监听:DB 变化 → stream → Domain User → UI
|
||||||
/// ```
|
/// ```
|
||||||
abstract class UserRepository {
|
abstract interface class UserRepository {
|
||||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 监听单个用户,DB 变化自动反映
|
/// 监听单个用户,DB 变化自动反映
|
||||||
@@ -53,23 +53,34 @@ abstract class UserRepository {
|
|||||||
|
|
||||||
// ── 写入 ─────────────────────────────────────────────────────────────────
|
// ── 写入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 保存完整用户(insert or replace)
|
/// 插入或替换单个用户
|
||||||
/// 调用方决定是否持久化
|
Future<void> insertOrReplaceUser(User user);
|
||||||
Future<void> saveUser(User user);
|
|
||||||
|
|
||||||
/// 批量保存用户(insert or replace)
|
/// 批量插入或替换用户
|
||||||
Future<void> saveUsers(List<User> users);
|
Future<void> insertOrReplaceUsers(List<User> users);
|
||||||
|
|
||||||
/// 仅更新指定字段,不影响其他列
|
/// 更新单个用户所有字段,按 uid 匹配
|
||||||
///
|
///
|
||||||
/// 示例:
|
/// 示例:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// await repo.updateFields(uid, UsersCompanion(
|
/// await repo.updateUser(user.copyWith(nickname: 'New Name'));
|
||||||
/// nickname: Value('New Name'),
|
|
||||||
/// lastOnline: Value(DateTime.now().millisecondsSinceEpoch),
|
|
||||||
/// ));
|
|
||||||
/// ```
|
/// ```
|
||||||
Future<void> updateFields(int uid, UsersCompanion companion);
|
Future<void> updateUser(User user);
|
||||||
|
|
||||||
|
/// 批量更新用户,每条按 uid 匹配更新所有字段
|
||||||
|
///
|
||||||
|
/// 示例:
|
||||||
|
/// ```dart
|
||||||
|
/// final updated = users.map((u) => u.copyWith(nickname: 'new')).toList();
|
||||||
|
/// await repo.updateUsersBatch(updated);
|
||||||
|
/// ```
|
||||||
|
Future<void> updateUsersBatch(List<User> users);
|
||||||
|
|
||||||
|
/// 单条 upsert
|
||||||
|
Future<void> upsertUser(User user);
|
||||||
|
|
||||||
|
/// 批量 upsert,走单次事务,优于循环调用 [upsertUser]
|
||||||
|
Future<void> upsertUsers(List<User> users);
|
||||||
|
|
||||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'package:im_app/domain/entities/user.dart';
|
|
||||||
import 'package:im_app/domain/repositories/user_repository.dart';
|
|
||||||
|
|
||||||
/// 批量插入用户用例
|
|
||||||
///
|
|
||||||
/// ## 职责
|
|
||||||
/// - 封装用户插入的业务规则
|
|
||||||
/// - 去重(uid 相同时保留最后一个)
|
|
||||||
/// - 分批插入,避免单次写入过大
|
|
||||||
///
|
|
||||||
/// ## 数据流
|
|
||||||
/// ```
|
|
||||||
/// ViewModel
|
|
||||||
/// → InsertUsersUseCase.execute(users)
|
|
||||||
/// → 去重
|
|
||||||
/// → UserRepository.saveUsers(chunk) ← 分批写入
|
|
||||||
/// → onProgress(completed, total) ← 可选进度回调
|
|
||||||
/// ← 实际插入数量
|
|
||||||
/// ```
|
|
||||||
class InsertUsersUseCase {
|
|
||||||
final UserRepository _repo;
|
|
||||||
static const _chunkSize = 200;
|
|
||||||
|
|
||||||
InsertUsersUseCase({required UserRepository userRepository})
|
|
||||||
: _repo = userRepository;
|
|
||||||
|
|
||||||
/// 批量插入用户
|
|
||||||
///
|
|
||||||
/// [users] 要插入的用户列表
|
|
||||||
/// [onProgress] 可选回调,每批完成后触发
|
|
||||||
///
|
|
||||||
/// 返回实际插入数量
|
|
||||||
Future<int> execute(
|
|
||||||
List<User> users, {
|
|
||||||
void Function(int completed, int total, List<User> chunk)? onProgress,
|
|
||||||
}) async {
|
|
||||||
if (users.isEmpty) return 0;
|
|
||||||
|
|
||||||
final deduped = {for (final u in users) u.uid: u}.values.toList();
|
|
||||||
final total = deduped.length;
|
|
||||||
int completed = 0;
|
|
||||||
|
|
||||||
while (completed < total) {
|
|
||||||
final end = (completed + _chunkSize).clamp(0, total);
|
|
||||||
final chunk = deduped.sublist(completed, end);
|
|
||||||
|
|
||||||
await _repo.saveUsers(chunk);
|
|
||||||
completed += chunk.length;
|
|
||||||
|
|
||||||
onProgress?.call(completed, total, chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return completed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
apps/im_app/lib/features/chat/call/di/call_log_provider.dart
Normal file
29
apps/im_app/lib/features/chat/call/di/call_log_provider.dart
Normal file
@@ -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<CallLogRepository>((ref) {
|
||||||
|
return CallLogRepositoryImpl(ref.watch(storageSdkProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Streams ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 监听所有通话记录
|
||||||
|
@riverpod
|
||||||
|
Stream<List<CallLog>> allCallLogs(Ref ref) {
|
||||||
|
return ref.watch(callLogRepositoryProvider).watchAllCallLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 监听指定通话记录
|
||||||
|
@riverpod
|
||||||
|
Stream<CallLog?> callLog(Ref ref, String id) {
|
||||||
|
return ref.watch(callLogRepositoryProvider).watchCallLog(id);
|
||||||
|
}
|
||||||
29
apps/im_app/lib/features/chat/di/chat_bot_provider.dart
Normal file
29
apps/im_app/lib/features/chat/di/chat_bot_provider.dart
Normal file
@@ -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<ChatBotRepository>((ref) {
|
||||||
|
return ChatBotRepositoryImpl(ref.watch(storageSdkProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Streams ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 监听所有聊天机器人
|
||||||
|
@riverpod
|
||||||
|
Stream<List<ChatBot>> allChatBots(Ref ref) {
|
||||||
|
return ref.watch(chatBotRepositoryProvider).watchAllChatBots();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 监听指定聊天机器人
|
||||||
|
@riverpod
|
||||||
|
Stream<ChatBot?> chatBot(Ref ref, int id) {
|
||||||
|
return ref.watch(chatBotRepositoryProvider).watchChatBot(id);
|
||||||
|
}
|
||||||
@@ -87,7 +87,7 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
|||||||
_loadNextPage();
|
_loadNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 测试 ──────────────────────────────────────────────────────────────────
|
// ── 测试:插入 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void startDBTest(BuildContext context) {
|
void startDBTest(BuildContext context) {
|
||||||
_isTesting = true;
|
_isTesting = true;
|
||||||
@@ -147,4 +147,67 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 测试:更新 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void updateFirst10(BuildContext context) {
|
||||||
|
_testDBUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _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} 条',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<List<User>> execute(List<User> users) async {
|
||||||
|
if (users.isEmpty) return [];
|
||||||
|
|
||||||
|
final targets = users.take(10).toList();
|
||||||
|
|
||||||
|
for (final user in targets) {
|
||||||
|
await _repo.deleteUser(user.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ class InsertUsersUseCase {
|
|||||||
final end = (completed + _chunkSize).clamp(0, total);
|
final end = (completed + _chunkSize).clamp(0, total);
|
||||||
final chunk = deduped.sublist(completed, end);
|
final chunk = deduped.sublist(completed, end);
|
||||||
|
|
||||||
await _repo.saveUsers(chunk);
|
await _repo.insertOrReplaceUsers(chunk);
|
||||||
completed += chunk.length;
|
completed += chunk.length;
|
||||||
|
|
||||||
onProgress?.call(completed, total, chunk);
|
onProgress?.call(completed, total, chunk);
|
||||||
|
|||||||
@@ -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<List<User>> execute(List<User> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,11 +62,25 @@ class _ChatDbTestPageState extends ConsumerState<ChatDbTestPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
AppButton.inverse(
|
Column(
|
||||||
label: testStarted ? '结束' : '开始',
|
children: [
|
||||||
onPressed: () => testStarted
|
AppButton.inverse(
|
||||||
? vm.stopDBTest(context)
|
label: "插入1万条数据",
|
||||||
: vm.startDBTest(context),
|
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),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Text(currentState, textAlign: TextAlign.end)),
|
Expanded(child: Text(currentState, textAlign: TextAlign.end)),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import 'package:im_app/app/di/network_provider.dart';
|
import 'package:im_app/app/di/network_provider.dart';
|
||||||
import 'package:im_app/app/di/db_provider.dart';
|
import 'package:im_app/app/di/db_provider.dart';
|
||||||
|
import 'package:im_app/app/di/user_provider.dart';
|
||||||
import 'package:im_app/data/repositories/auth_repository_impl.dart';
|
import 'package:im_app/data/repositories/auth_repository_impl.dart';
|
||||||
import 'package:im_app/domain/repositories/auth_repository.dart';
|
import 'package:im_app/domain/repositories/auth_repository.dart';
|
||||||
import 'package:im_app/features/login/usecases/login_usecase.dart';
|
import 'package:im_app/features/login/usecases/login_usecase.dart';
|
||||||
@@ -23,6 +24,7 @@ import 'package:im_app/features/login/usecases/login_usecase.dart';
|
|||||||
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
|
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
|
||||||
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
||||||
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
|
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
|
||||||
|
/// → ref.read(userRepositoryProvider) ← app/di/ 手动装配
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
// ── Repository ────────────────────────────────────────────────────────────────
|
// ── Repository ────────────────────────────────────────────────────────────────
|
||||||
@@ -41,7 +43,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
// TODO: final secureStorage = ref.read(secureStorageProvider);
|
// TODO: final secureStorage = ref.read(secureStorageProvider);
|
||||||
|
|
||||||
return AuthRepositoryImpl(
|
return AuthRepositoryImpl(
|
||||||
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
client: ref.read(networkSdkApiProvider),
|
||||||
onTokenUpdate: (token) {
|
onTokenUpdate: (token) {
|
||||||
apiConfig.updateToken(token); // 内存(network_sdk)
|
apiConfig.updateToken(token); // 内存(network_sdk)
|
||||||
// TODO: secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
// TODO: secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||||
@@ -53,12 +55,13 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
|
|
||||||
/// 登录用例 Provider
|
/// 登录用例 Provider
|
||||||
///
|
///
|
||||||
/// 多步编排:格式校验 → 调接口 → 写 Token → 连接 WebSocket → 打开数据库
|
/// 多步编排:格式校验 → 调接口 → 写 Token → 连接 WebSocket → 打开数据库 → 持久化用户
|
||||||
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
||||||
return LoginUseCase(
|
return LoginUseCase(
|
||||||
authRepository: ref.read(authRepositoryProvider),
|
authRepository: ref.read(authRepositoryProvider),
|
||||||
socketManager: ref.read(socketManagerProvider),
|
socketManager: ref.read(socketManagerProvider),
|
||||||
apiConfig: ref.read(apiConfigProvider),
|
apiConfig: ref.read(apiConfigProvider),
|
||||||
storageApi: ref.read(storageSdkProvider),
|
storageApi: ref.read(storageSdkProvider),
|
||||||
|
userRepository: ref.read(userRepositoryProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,11 +72,13 @@ class LoginViewModel extends _$LoginViewModel {
|
|||||||
state = state.copyWith(isLoading: true, error: null);
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(loginUseCaseProvider).verifyAndLogin(
|
await ref
|
||||||
countryCode: state.countryCode,
|
.read(loginUseCaseProvider)
|
||||||
contact: state.contact,
|
.verifyAndLogin(
|
||||||
code: code,
|
countryCode: state.countryCode,
|
||||||
);
|
contact: state.contact,
|
||||||
|
code: code,
|
||||||
|
);
|
||||||
|
|
||||||
// 成功后触发路由守卫重定向。
|
// 成功后触发路由守卫重定向。
|
||||||
// 注意:login() 触发导航后 provider 随即被 dispose,之后不能再写 state。
|
// 注意:login() 触发导航后 provider 随即被 dispose,之后不能再写 state。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:storage_sdk/storage_sdk.dart';
|
|||||||
import 'package:im_app/core/services/socket_manager.dart';
|
import 'package:im_app/core/services/socket_manager.dart';
|
||||||
import 'package:im_app/domain/entities/user.dart';
|
import 'package:im_app/domain/entities/user.dart';
|
||||||
import 'package:im_app/domain/repositories/auth_repository.dart';
|
import 'package:im_app/domain/repositories/auth_repository.dart';
|
||||||
|
import 'package:im_app/domain/repositories/user_repository.dart';
|
||||||
|
|
||||||
/// 登录用例
|
/// 登录用例
|
||||||
///
|
///
|
||||||
@@ -31,6 +32,7 @@ import 'package:im_app/domain/repositories/auth_repository.dart';
|
|||||||
/// → AuthRepository.login() → User + token
|
/// → AuthRepository.login() → User + token
|
||||||
/// → SocketManager.connect(token)
|
/// → SocketManager.connect(token)
|
||||||
/// → StorageSdkApi.openDatabase(uid)
|
/// → StorageSdkApi.openDatabase(uid)
|
||||||
|
/// → UserRepository.insertOrReplaceUser(user)
|
||||||
/// ← User
|
/// ← User
|
||||||
/// ```
|
/// ```
|
||||||
class LoginUseCase {
|
class LoginUseCase {
|
||||||
@@ -38,6 +40,7 @@ class LoginUseCase {
|
|||||||
final SocketManager _socketManager;
|
final SocketManager _socketManager;
|
||||||
final ApiConfig _apiConfig;
|
final ApiConfig _apiConfig;
|
||||||
final StorageSdkApi _storageApi;
|
final StorageSdkApi _storageApi;
|
||||||
|
final UserRepository _userRepository;
|
||||||
|
|
||||||
StorageSdkLifecycle get _storageLifeCycle =>
|
StorageSdkLifecycle get _storageLifeCycle =>
|
||||||
_storageApi as StorageSdkLifecycle;
|
_storageApi as StorageSdkLifecycle;
|
||||||
@@ -47,10 +50,12 @@ class LoginUseCase {
|
|||||||
required SocketManager socketManager,
|
required SocketManager socketManager,
|
||||||
required ApiConfig apiConfig,
|
required ApiConfig apiConfig,
|
||||||
required StorageSdkApi storageApi,
|
required StorageSdkApi storageApi,
|
||||||
|
required UserRepository userRepository,
|
||||||
}) : _authRepository = authRepository,
|
}) : _authRepository = authRepository,
|
||||||
_socketManager = socketManager,
|
_socketManager = socketManager,
|
||||||
_apiConfig = apiConfig,
|
_apiConfig = apiConfig,
|
||||||
_storageApi = storageApi;
|
_storageApi = storageApi,
|
||||||
|
_userRepository = userRepository;
|
||||||
|
|
||||||
/// 步骤 1:发送手机验证码
|
/// 步骤 1:发送手机验证码
|
||||||
///
|
///
|
||||||
@@ -62,15 +67,12 @@ class LoginUseCase {
|
|||||||
required String contact,
|
required String contact,
|
||||||
}) async {
|
}) async {
|
||||||
_validatePhone(contact);
|
_validatePhone(contact);
|
||||||
await _authRepository.sendOtp(
|
await _authRepository.sendOtp(countryCode: countryCode, contact: contact);
|
||||||
countryCode: countryCode,
|
|
||||||
contact: contact,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 步骤 2+3:校验验证码并完成登录,返回 [User]
|
/// 步骤 2+3:校验验证码并完成登录,返回 [User]
|
||||||
///
|
///
|
||||||
/// 内部串行:verifyOtp → login → connectWebSocket → openDatabase
|
/// 内部串行:verifyOtp → login → connectWebSocket → openDatabase → saveUser
|
||||||
///
|
///
|
||||||
/// 抛出:
|
/// 抛出:
|
||||||
/// - [FormatException] — 验证码格式不合法
|
/// - [FormatException] — 验证码格式不合法
|
||||||
@@ -105,6 +107,9 @@ class LoginUseCase {
|
|||||||
// 按用户 uid 打开本地数据库
|
// 按用户 uid 打开本地数据库
|
||||||
await _storageLifeCycle.openDatabase(user.uid);
|
await _storageLifeCycle.openDatabase(user.uid);
|
||||||
|
|
||||||
|
// 持久化登录用户信息
|
||||||
|
await _userRepository.insertOrReplaceUser(user);
|
||||||
|
|
||||||
// TODO: 扩展点 — 同步联系人列表、注册推送 token
|
// TODO: 扩展点 — 同步联系人列表、注册推送 token
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -56,11 +56,26 @@ abstract class StorageSdkApi {
|
|||||||
factory StorageSdkApi({
|
factory StorageSdkApi({
|
||||||
required GeneratedDatabase Function(QueryExecutor) databaseFactory,
|
required GeneratedDatabase Function(QueryExecutor) databaseFactory,
|
||||||
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
|
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
|
||||||
}) =>
|
}) => StorageSdkWiring.build(
|
||||||
StorageSdkWiring.build(
|
databaseFactory: databaseFactory,
|
||||||
databaseFactory: databaseFactory,
|
tableRegistry: tableRegistry,
|
||||||
tableRegistry: tableRegistry,
|
);
|
||||||
);
|
|
||||||
|
// ── 事务 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 在单个事务内执行 [action]。
|
||||||
|
///
|
||||||
|
/// - 成功则自动 COMMIT
|
||||||
|
/// - 抛出异常则自动 ROLLBACK,异常会继续向上传递
|
||||||
|
///
|
||||||
|
/// 示例:
|
||||||
|
/// ```dart
|
||||||
|
/// await _storage.transaction(() async {
|
||||||
|
/// await _storage.insertOrReplace(companionA);
|
||||||
|
/// await _storage.updateWhere(companionB, (t) => t.uid.equals(1));
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
Future<T> transaction<T>(Future<T> Function() action);
|
||||||
|
|
||||||
// ── 插入 ─────────────────────────────────────────────────────────────────
|
// ── 插入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -92,16 +107,16 @@ abstract class StorageSdkApi {
|
|||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
Future<void> updateWhere<D, T extends Table>(
|
Future<void> updateWhere<D, T extends Table>(
|
||||||
Insertable<D> companion,
|
Insertable<D> companion,
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 按条件删除。
|
/// 按条件删除。
|
||||||
Future<void> deleteWhere<D, T extends Table>(
|
Future<void> deleteWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 清空整张表。
|
/// 清空整张表。
|
||||||
Future<void> deleteAll<D>();
|
Future<void> deleteAll<D>();
|
||||||
@@ -118,13 +133,13 @@ abstract class StorageSdkApi {
|
|||||||
|
|
||||||
/// 按条件查询。
|
/// 按条件查询。
|
||||||
Future<List<D>> selectWhere<D, T extends Table>(
|
Future<List<D>> selectWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 查询第一条匹配记录。
|
/// 查询第一条匹配记录。
|
||||||
Future<D?> selectFirst<D, T extends Table>(
|
Future<D?> selectFirst<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -133,13 +148,13 @@ abstract class StorageSdkApi {
|
|||||||
|
|
||||||
/// 按条件监听(实时流)。
|
/// 按条件监听(实时流)。
|
||||||
Stream<List<D>> watchWhere<D, T extends Table>(
|
Stream<List<D>> watchWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 监听第一条匹配记录(实时流)。
|
/// 监听第一条匹配记录(实时流)。
|
||||||
Stream<D?> watchFirst<D, T extends Table>(
|
Stream<D?> watchFirst<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -152,7 +167,5 @@ abstract class StorageSdkApi {
|
|||||||
// ── 统计 ─────────────────────────────────────────────────────────────────
|
// ── 统计 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 统计记录数。
|
/// 统计记录数。
|
||||||
Future<int> count<D, T extends Table>({
|
Future<int> count<D, T extends Table>({Expression<bool> Function(T)? filter});
|
||||||
Expression<bool> Function(T)? filter,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
|
|||||||
StorageSdkApiImpl({
|
StorageSdkApiImpl({
|
||||||
required StorageSdkCore core,
|
required StorageSdkCore core,
|
||||||
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
|
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
|
||||||
}) : _core = core,
|
}) : _core = core,
|
||||||
_tableRegistry = tableRegistry;
|
_tableRegistry = tableRegistry;
|
||||||
|
|
||||||
// ── 表查找 ───────────────────────────────────────────────────────────────
|
// ── 表查找 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -25,6 +25,13 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
|
|||||||
return table as TableInfo<T, D>;
|
return table as TableInfo<T, D>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前已开启的数据库实例,未开启则抛出 [StateError]。
|
||||||
|
GeneratedDatabase get _db {
|
||||||
|
final db = _core.dataSource.current;
|
||||||
|
if (db == null) throw StateError('数据库未开启,请先调用 openDatabase()');
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 生命周期 ─────────────────────────────────────────────────────────────
|
// ── 生命周期 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,6 +43,16 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
|
|||||||
@override
|
@override
|
||||||
bool get isDatabaseOpen => _core.dataSource.current != null;
|
bool get isDatabaseOpen => _core.dataSource.current != null;
|
||||||
|
|
||||||
|
// ── 事务 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 在单个事务内执行 [action]。
|
||||||
|
///
|
||||||
|
/// - 成功则自动 COMMIT
|
||||||
|
/// - 抛出异常则自动 ROLLBACK,异常会继续向上传递
|
||||||
|
@override
|
||||||
|
Future<T> transaction<T>(Future<T> Function() action) =>
|
||||||
|
_db.transaction(action);
|
||||||
|
|
||||||
// ── 插入 ─────────────────────────────────────────────────────────────────
|
// ── 插入 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,64 +71,57 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateWhere<D, T extends Table>(
|
Future<void> updateWhere<D, T extends Table>(
|
||||||
Insertable<D> companion,
|
Insertable<D> companion,
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.updateWhere(_tableFor<T, D>(), companion, filter);
|
||||||
_core.repo.updateWhere(_tableFor<T, D>(), companion, filter);
|
|
||||||
|
|
||||||
// ── 删除 ─────────────────────────────────────────────────────────────────
|
// ── 删除 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteWhere<D, T extends Table>(
|
Future<void> deleteWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.deleteWhere(_tableFor<T, D>(), filter);
|
||||||
_core.repo.deleteWhere(_tableFor<T, D>(), filter);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteAll<D>() =>
|
Future<void> deleteAll<D>() => _core.repo.deleteAll(_tableFor<Table, D>());
|
||||||
_core.repo.deleteAll(_tableFor<Table, D>());
|
|
||||||
|
|
||||||
// ── 查询 ─────────────────────────────────────────────────────────────────
|
// ── 查询 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<D>> selectAll<D>() =>
|
Future<List<D>> selectAll<D>() => _core.repo.selectAll(_tableFor<Table, D>());
|
||||||
_core.repo.selectAll(_tableFor<Table, D>());
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<D>> selectWhere<D, T extends Table>(
|
Future<List<D>> selectWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.selectWhere(_tableFor<T, D>(), filter);
|
||||||
_core.repo.selectWhere(_tableFor<T, D>(), filter);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<D?> selectFirst<D, T extends Table>(
|
Future<D?> selectFirst<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.selectFirst(_tableFor<T, D>(), filter);
|
||||||
_core.repo.selectFirst(_tableFor<T, D>(), filter);
|
|
||||||
|
|
||||||
// ── 监听 ─────────────────────────────────────────────────────────────────
|
// ── 监听 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<D>> watchAll<D>() =>
|
Stream<List<D>> watchAll<D>() => _core.repo.watchAll(_tableFor<Table, D>());
|
||||||
_core.repo.watchAll(_tableFor<Table, D>());
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<D>> watchWhere<D, T extends Table>(
|
Stream<List<D>> watchWhere<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.watchWhere(_tableFor<T, D>(), filter);
|
||||||
_core.repo.watchWhere(_tableFor<T, D>(), filter);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<D?> watchFirst<D, T extends Table>(
|
Stream<D?> watchFirst<D, T extends Table>(
|
||||||
Expression<bool> Function(T) filter,
|
Expression<bool> Function(T) filter,
|
||||||
) =>
|
) => _core.repo.watchFirst(_tableFor<T, D>(), filter);
|
||||||
_core.repo.watchFirst(_tableFor<T, D>(), filter);
|
|
||||||
|
|
||||||
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
// ── 原始 SQL ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<QueryRow>> rawQuery(String sql, [List<Object?> args = const []]) =>
|
Future<List<QueryRow>> rawQuery(
|
||||||
_core.repo.rawQuery(sql, args);
|
String sql, [
|
||||||
|
List<Object?> args = const [],
|
||||||
|
]) => _core.repo.rawQuery(sql, args);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> rawExecute(String sql, [List<Object?> args = const []]) =>
|
Future<void> rawExecute(String sql, [List<Object?> args = const []]) =>
|
||||||
@@ -122,6 +132,5 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
|
|||||||
@override
|
@override
|
||||||
Future<int> count<D, T extends Table>({
|
Future<int> count<D, T extends Table>({
|
||||||
Expression<bool> Function(T)? filter,
|
Expression<bool> Function(T)? filter,
|
||||||
}) =>
|
}) => _core.repo.count(_tableFor<T, D>(), filter: filter);
|
||||||
_core.repo.count(_tableFor<T, D>(), filter: filter);
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user