diff --git a/apps/im_app/lib/app/di/db_provider.dart b/apps/im_app/lib/app/di/db_provider.dart index 05c86c7..ba51c82 100644 --- a/apps/im_app/lib/app/di/db_provider.dart +++ b/apps/im_app/lib/app/di/db_provider.dart @@ -11,14 +11,22 @@ import '../../data/local/drift/app_database.dart'; /// 用法: /// ```dart /// // 登录后开库 -/// await ref.read(storageSdkProvider).openDatabase(user.id); +/// await ref.read(storageSdkLifecycleProvider).openDatabase(user.id); /// /// // CRUD 示例 /// final db = ref.read(storageSdkProvider); -/// await db.insertOrReplace(appDb.users, companion); +/// await db.insertOrReplace(companion); +/// final users = await db.selectAll(); /// ``` + final storageSdkProvider = Provider((ref) { return StorageSdkApi( databaseFactory: (executor) => AppDatabase(executor), + tableRegistry: (db) => AppDatabase.getTableRegistry(db), ); }); + +/// 生命周期管理,仅供登录/登出使用。 +final storageSdkLifecycleProvider = Provider((ref) { + return ref.read(storageSdkProvider) as StorageSdkLifecycle; +}); \ No newline at end of file diff --git a/apps/im_app/lib/app/router/app_route_name.dart b/apps/im_app/lib/app/router/app_route_name.dart index 8b20c4f..0f8ddbd 100644 --- a/apps/im_app/lib/app/router/app_route_name.dart +++ b/apps/im_app/lib/app/router/app_route_name.dart @@ -64,6 +64,7 @@ enum AppRouteName { chatDetail('/chat/detail'), // 路径参数形式:导航用 AppRouteName.chatDetailByIdPath(id),不直接用 .path chatDetailById('/chat/:id'), + chatDBTest('/chat/dbTest'), // ── Settings 子路由 ─────────────────────────────────────────────────────── settingsTheme('/settings/theme'), diff --git a/apps/im_app/lib/app/router/app_router.dart b/apps/im_app/lib/app/router/app_router.dart index faae228..2a46496 100644 --- a/apps/im_app/lib/app/router/app_router.dart +++ b/apps/im_app/lib/app/router/app_router.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:im_app/features/chat/view/chat_db_test_page.dart'; import '../../features/app_tab/view/app_tab.dart'; import '../../features/chat/view/chat_detail_page.dart'; @@ -115,6 +116,13 @@ final routerProvider = Provider((ref) { // parentNavigatorKey: _rootKey 确保路由覆盖 Shell,TabBar 消失 // // extra 传参:接收 ({String conversationId, String title}) + GoRoute( + parentNavigatorKey: _rootKey, + path: AppRouteName.chatDBTest.path, + builder: (context, state) { + return const ChatDbTestPage(); + }, + ), GoRoute( parentNavigatorKey: _rootKey, path: AppRouteName.chatDetail.path, diff --git a/apps/im_app/lib/app/router/guards/auth_guard.dart b/apps/im_app/lib/app/router/guards/auth_guard.dart index bdcf195..1636457 100644 --- a/apps/im_app/lib/app/router/guards/auth_guard.dart +++ b/apps/im_app/lib/app/router/guards/auth_guard.dart @@ -34,13 +34,13 @@ String? authGuard(AuthNotifier authNotifier, GoRouterState state) { case AppRouteName.login: // 已登录还在登录页 → 跳聊天页 return isLoggedIn ? AppRouteName.chat.path : null; - case AppRouteName.chat: case AppRouteName.chatDetail: case AppRouteName.chatDetailById: case AppRouteName.contact: case AppRouteName.settings: case AppRouteName.settingsTheme: + case AppRouteName.chatDBTest: // 受保护路由 → 未登录跳登录页 return isLoggedIn ? null : AppRouteName.login.path; } diff --git a/apps/im_app/lib/data/local/drift/app_database.dart b/apps/im_app/lib/data/local/drift/app_database.dart index 98949e3..d854481 100644 --- a/apps/im_app/lib/data/local/drift/app_database.dart +++ b/apps/im_app/lib/data/local/drift/app_database.dart @@ -1,10 +1,23 @@ import 'package:drift/drift.dart'; import 'package:im_app/data/local/drift/tables/users.dart'; +import 'package:im_app/data/local/drift/tables/test_tables.dart'; part 'app_database.g.dart'; -@DriftDatabase(tables: [Users]) +@DriftDatabase(tables: [Users,TestTables]) //update mapping here class AppDatabase extends _$AppDatabase { + + static Map getTableRegistry(GeneratedDatabase database) { + if (database is! AppDatabase) { + return { + }; + } + return { + User: database.users, + TestTable: database.testTables, + }; + } + AppDatabase(super.e); @override @@ -37,4 +50,6 @@ class AppDatabase extends _$AppDatabase { }, ); } + + } diff --git a/apps/im_app/lib/data/local/drift/tables/test_tables.dart b/apps/im_app/lib/data/local/drift/tables/test_tables.dart new file mode 100644 index 0000000..3d9f9ca --- /dev/null +++ b/apps/im_app/lib/data/local/drift/tables/test_tables.dart @@ -0,0 +1,41 @@ +import 'package:drift/drift.dart'; + +@DataClassName('TestTable') +class TestTables extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get uid => integer().nullable()(); + TextColumn get uuid => text().nullable()(); + IntColumn get lastOnline => integer().nullable()(); + TextColumn get profilePic => text().nullable()(); + TextColumn get profilePicGaussian => text().withDefault(const Constant(''))(); + TextColumn get nickname => text().nullable()(); + TextColumn get depositName => text().nullable()(); + IntColumn get hasSetDepositName => integer().withDefault(const Constant(0))(); + TextColumn get contact => text().nullable()(); + TextColumn get countryCode => text().nullable()(); + TextColumn get username => text().nullable()(); + IntColumn get role => integer().nullable()(); + IntColumn get relationship => integer().nullable()(); + IntColumn get friendStatus => integer().nullable()(); + TextColumn get bio => text().nullable()(); + TextColumn get userAlias => text().nullable()(); + IntColumn get requestAt => integer().nullable()(); + IntColumn get deletedAt => integer().nullable()(); + TextColumn get email => text().nullable()(); + TextColumn get recoveryEmail => text().nullable()(); + TextColumn get remark => text().nullable()(); + TextColumn get source => text().nullable()(); + IntColumn get addIndex => integer().nullable()(); + IntColumn get incomingSoundId => integer().withDefault(const Constant(0))(); + IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))(); + IntColumn get notificationSoundId => integer().withDefault(const Constant(0))(); + IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))(); + IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))(); + TextColumn get groupTags => text().withDefault(const Constant('[]'))(); + TextColumn get friendTags => text().withDefault(const Constant('[]'))(); + TextColumn get publicKey => text().nullable()(); + IntColumn get configBits => integer().withDefault(const Constant(0))(); + TextColumn get hint => text().nullable()(); + @override + String get tableName => 'test_tables'; +} \ No newline at end of file diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart new file mode 100644 index 0000000..da6fa16 --- /dev/null +++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart @@ -0,0 +1,119 @@ +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:flutter/foundation.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: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 String currentState; + + const ChatDbTestState({ + this.testStarted = false, + this.testResults = const [], + this.currentState = '', + }); + + ChatDbTestState copyWith({ + bool? testStarted, + List? testResults, + String? currentState, + }) => ChatDbTestState( + testStarted: testStarted ?? this.testStarted, + testResults: testResults ?? this.testResults, + currentState: currentState ?? this.currentState, + ); +} + +@riverpod +class ChatDbTestViewModel extends _$ChatDbTestViewModel { + + @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); + } + + // ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ────────────────────────── + + /// 开始测试 + void startDBTest(BuildContext context) { + state = state.copyWith(testStarted: true, currentState: '开始测试'); + _testDBInsert(); + } + + /// 结束测试 + void stopDBTest(BuildContext context) { + state = state.copyWith(testStarted: false, currentState: '结束测试'); + } + + Future _testDBInsert() async { + final db = ref.read(storageSdkProvider); + const count = 10000; + const chunkSize = 50; + final stopwatch = Stopwatch()..start(); + + debugPrint('开始测试: $count 条,每批 $chunkSize 条'); + + int completed = 0; + + for (var i = 0; i < count; i += chunkSize) { + final chunk = List.generate( + chunkSize.clamp(0, count - i), + (j) => UsersCompanion.insert( + uid: Value(i + j), + nickname: Value('User ${i + j}'), + ), + ); + + await db.batchInsertOrReplace(chunk); + completed += chunk.length; + + // 让出主线程 + await Future.delayed(Duration.zero); + + debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)'); + + // 更新 UI 状态 + if (ref.mounted) { + state = state.copyWith( + currentState: '已插入 $completed / $count 条', + ); + } + } + + debugPrint('全部完成: ${stopwatch.elapsedMilliseconds}ms'); + if (ref.mounted) { + state = state.copyWith( + testStarted: false, + currentState: '完成!共 $count 条,耗时 ${stopwatch.elapsedMilliseconds}ms', + ); + } + } +} \ No newline at end of file diff --git a/apps/im_app/lib/features/chat/presentation/chat_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_view_model.dart index 8cea4fd..b5589e1 100644 --- a/apps/im_app/lib/features/chat/presentation/chat_view_model.dart +++ b/apps/im_app/lib/features/chat/presentation/chat_view_model.dart @@ -55,6 +55,11 @@ class ChatViewModel extends _$ChatViewModel { context.go(AppRouteName.settings.path); } + /// 测试数据库性能 + void goToDatabaseTest(BuildContext context) { + context.push(AppRouteName.chatDBTest.path); + } + // ── 业务 ───────────────────────────────────────────────────────────────── /// 退出登录 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 new file mode 100644 index 0000000..06ec586 --- /dev/null +++ b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +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 { + const ChatDbTestPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final vm = ref.read(chatDbTestViewModelProvider.notifier); + final state = ref.watch(chatDbTestViewModelProvider); + + return Scaffold( + appBar: AppBar(title: const Text('测试数据库'), ), + body: Column( + mainAxisSize: MainAxisSize.max, + spacing: 16, + children: [ + SizedBox(height: 4), + Padding( + padding: EdgeInsetsGeometry.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppButton.inverse( + label: state.testStarted ? '结束' : '开始', + onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context), + ), + SizedBox(width: 8), + Expanded( + child: Text( + state.currentState, + textAlign: TextAlign.end, + ), + ) + ], + ) + ), + Expanded( + child: ListView.builder( + itemCount: state.testResults.length, + 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), + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/apps/im_app/lib/features/chat/view/chat_page.dart b/apps/im_app/lib/features/chat/view/chat_page.dart index bfa9db8..ce264a2 100644 --- a/apps/im_app/lib/features/chat/view/chat_page.dart +++ b/apps/im_app/lib/features/chat/view/chat_page.dart @@ -54,6 +54,10 @@ class ChatPage extends ConsumerWidget { label: '无参 go', onPressed: () => vm.goToSettings(context), ), + AppButton.inverse( + label: '测试数据库性能', + onPressed: () => vm.goToDatabaseTest(context), + ), AppButton.secondary( label: '退出登录', fullWidth: false, diff --git a/packages/im_log_sdk/build.yaml b/packages/im_log_sdk/build.yaml new file mode 100644 index 0000000..e69de29 diff --git a/packages/storage_sdk/lib/src/data/local/datasources/database_datasource.dart b/packages/storage_sdk/lib/src/data/local/datasources/database_datasource.dart index 6f49cbd..cb6b1fa 100644 --- a/packages/storage_sdk/lib/src/data/local/datasources/database_datasource.dart +++ b/packages/storage_sdk/lib/src/data/local/datasources/database_datasource.dart @@ -45,7 +45,7 @@ class DatabaseDataSource { } final file = File('${dbDir.path}/$uid.sqlite'); - _db = _databaseFactory(NativeDatabase(file)); + _db = _databaseFactory(NativeDatabase.createInBackground(file)); return _db!; } diff --git a/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart b/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart index 42ae2be..70b2015 100644 --- a/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart +++ b/packages/storage_sdk/lib/src/presentation/facade/storage_sdk_api.dart @@ -4,11 +4,14 @@ import '../wiring/storage_sdk_wiring.dart'; /// 本地数据库的统一公开接口。 /// -/// 通过 [StorageSdkApi] 工厂方法获取实例,传入 App 侧的数据库工厂即可: +/// 通过 [StorageSdkApi] 工厂方法获取实例,传入 App 侧的数据库工厂和表注册表即可: /// /// ```dart /// final api = StorageSdkApi( /// databaseFactory: (executor) => AppDatabase(executor), +/// tableRegistry: (db) => { +/// User: (db as AppDatabase).users, +/// }, /// ); /// ``` /// @@ -19,98 +22,124 @@ import '../wiring/storage_sdk_wiring.dart'; /// /// 内部接口,仅供 App 层生命周期管理使用 abstract class StorageSdkLifecycle { + /// 打开指定用户的数据库(按 uid 隔离文件)。 Future openDatabase(int uid); + + /// 关闭当前数据库连接。 Future closeDatabase(); + + /// 是否已开库。 bool get isDatabaseOpen; } -/// -/// + /// ## CRUD -/// 所有 CRUD 方法均为泛型,接受 Drift 的 [TableInfo],与具体业务表解耦。 -/// App 层传入自己定义的 Table 即可复用全部操作。 +/// 所有 CRUD 方法均通过泛型数据类型查找对应表,与具体业务表解耦。 +/// App 层通过 [tableRegistry] 注入表映射,无需在调用时传入 [TableInfo]。 abstract class StorageSdkApi { /// 创建 SDK 实例。 /// /// [databaseFactory] 由 App 层提供:接受 [QueryExecutor], /// 返回自定义的 [GeneratedDatabase] 子类(含表定义和迁移策略)。 + /// + /// [tableRegistry] 由 App 层提供:将数据类映射到对应的表信息, + /// storage_sdk 不感知具体表结构。 + /// + /// 示例: + /// ```dart + /// final api = StorageSdkApi( + /// databaseFactory: (executor) => AppDatabase(executor), + /// tableRegistry: (db) => { + /// User: (db as AppDatabase).users, + /// }, + /// ); + /// ``` factory StorageSdkApi({ required GeneratedDatabase Function(QueryExecutor) databaseFactory, + required Map Function(GeneratedDatabase) tableRegistry, }) => - StorageSdkWiring.build(databaseFactory: databaseFactory); + StorageSdkWiring.build( + databaseFactory: databaseFactory, + tableRegistry: tableRegistry, + ); // ── 插入 ───────────────────────────────────────────────────────────────── /// 插入或替换(主键冲突时覆盖)。 - Future insertOrReplace( - TableInfo table, - Insertable companion, - ); + /// + /// 示例: + /// ```dart + /// await sdk.insertOrReplace( + /// UsersCompanion.insert(nickname: Value('Edmund')), + /// ); + /// ``` + Future insertOrReplace(Insertable companion); /// 插入或忽略(主键冲突时跳过)。 - Future insert( - TableInfo table, - Insertable companion, - ); + Future insert(Insertable companion); /// 批量插入或替换。 - Future batchInsertOrReplace( - TableInfo table, - List> companions, - ); + Future batchInsertOrReplace(List> companions); // ── 更新 ───────────────────────────────────────────────────────────────── /// 按条件更新。 - Future updateWhere( - TableInfo table, - Insertable companion, - Expression Function(T) filter, - ); + /// + /// 示例: + /// ```dart + /// await sdk.updateWhere( + /// UsersCompanion(nickname: Value('NewName')), + /// (t) => t.uid.equals(123), + /// ); + /// ``` + Future updateWhere( + Insertable companion, + Expression Function(T) filter, + ); // ── 删除 ───────────────────────────────────────────────────────────────── /// 按条件删除。 - Future deleteWhere( - TableInfo table, - Expression Function(T) filter, - ); + Future deleteWhere( + Expression Function(T) filter, + ); /// 清空整张表。 - Future deleteAll(TableInfo table); + Future deleteAll(); // ── 查询 ───────────────────────────────────────────────────────────────── /// 查询全部记录。 - Future> selectAll(TableInfo table); + /// + /// 示例: + /// ```dart + /// final users = await sdk.selectAll(); + /// ``` + Future> selectAll(); /// 按条件查询。 - Future> selectWhere( - TableInfo table, - Expression Function(T) filter, - ); + Future> selectWhere( + Expression Function(T) filter, + ); /// 查询第一条匹配记录。 - Future selectFirst( - TableInfo table, - Expression Function(T) filter, - ); + Future selectFirst( + Expression Function(T) filter, + ); // ── 监听 ───────────────────────────────────────────────────────────────── /// 监听全部记录(实时流)。 - Stream> watchAll(TableInfo table); + Stream> watchAll(); /// 按条件监听(实时流)。 - Stream> watchWhere( - TableInfo table, - Expression Function(T) filter, - ); + Stream> watchWhere( + Expression Function(T) filter, + ); /// 监听第一条匹配记录(实时流)。 - Stream watchFirst( - TableInfo table, - Expression Function(T) filter, - ); + Stream watchFirst( + Expression Function(T) filter, + ); // ── 原始 SQL ───────────────────────────────────────────────────────────── @@ -123,8 +152,7 @@ abstract class StorageSdkApi { // ── 统计 ───────────────────────────────────────────────────────────────── /// 统计记录数。 - Future count( - TableInfo table, { + Future count({ Expression Function(T)? filter, }); -} +} \ No newline at end of file diff --git a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart index 0257976..cdbf4cb 100644 --- a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart +++ b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_api_impl.dart @@ -6,14 +6,29 @@ import 'storage_sdk_core.dart'; /// [StorageSdkApi] 的实现,委托给 [StorageSdkCore]。 class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { final StorageSdkCore _core; + final Map Function(GeneratedDatabase) _tableRegistry; - StorageSdkApiImpl({required StorageSdkCore core}) : _core = core; + StorageSdkApiImpl({ + required StorageSdkCore core, + required Map Function(GeneratedDatabase) tableRegistry, + }) : _core = core, + _tableRegistry = tableRegistry; + + // ── 表查找 ─────────────────────────────────────────────────────────────── + + /// 根据泛型数据类型从注册表中查找对应的 [TableInfo]。 + TableInfo _tableFor() { + final db = _core.dataSource.current; + if (db == null) throw StateError('数据库未开启,请先调用 openDatabase()'); + final table = _tableRegistry(db)[D]; + if (table == null) throw StateError('未注册类型 $D 对应的表,请检查 tableRegistry'); + return table as TableInfo; + } // ── 生命周期 ───────────────────────────────────────────────────────────── @override - Future openDatabase(int uid) => - _core.dataSource.openDatabase(uid); + Future openDatabase(int uid) => _core.dataSource.openDatabase(uid); @override Future closeDatabase() => _core.dataSource.closeDatabase(); @@ -24,88 +39,73 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { // ── 插入 ───────────────────────────────────────────────────────────────── @override - Future insertOrReplace( - TableInfo table, - Insertable companion, - ) => - _core.repo.insertOrReplace(table, companion); + Future insertOrReplace(Insertable companion) => + _core.repo.insertOrReplace(_tableFor(), companion); @override - Future insert( - TableInfo table, - Insertable companion, - ) => - _core.repo.insert(table, companion); + Future insert(Insertable companion) => + _core.repo.insert(_tableFor(), companion); @override - Future batchInsertOrReplace( - TableInfo table, - List> companions, - ) => - _core.repo.batchInsertOrReplace(table, companions); + Future batchInsertOrReplace(List> companions) => + _core.repo.batchInsertOrReplace(_tableFor(), companions); // ── 更新 ───────────────────────────────────────────────────────────────── @override - Future updateWhere( - TableInfo table, - Insertable companion, - Expression Function(T) filter, - ) => - _core.repo.updateWhere(table, companion, filter); + Future updateWhere( + Insertable companion, + Expression Function(T) filter, + ) => + _core.repo.updateWhere(_tableFor(), companion, filter); // ── 删除 ───────────────────────────────────────────────────────────────── @override - Future deleteWhere( - TableInfo table, - Expression Function(T) filter, - ) => - _core.repo.deleteWhere(table, filter); + Future deleteWhere( + Expression Function(T) filter, + ) => + _core.repo.deleteWhere(_tableFor(), filter); @override - Future deleteAll(TableInfo table) => - _core.repo.deleteAll(table); + Future deleteAll() => + _core.repo.deleteAll(_tableFor()); // ── 查询 ───────────────────────────────────────────────────────────────── @override - Future> selectAll(TableInfo table) => - _core.repo.selectAll(table); + Future> selectAll() => + _core.repo.selectAll(_tableFor()); @override - Future> selectWhere( - TableInfo table, - Expression Function(T) filter, - ) => - _core.repo.selectWhere(table, filter); + Future> selectWhere( + Expression Function(T) filter, + ) => + _core.repo.selectWhere(_tableFor(), filter); @override - Future selectFirst( - TableInfo table, - Expression Function(T) filter, - ) => - _core.repo.selectFirst(table, filter); + Future selectFirst( + Expression Function(T) filter, + ) => + _core.repo.selectFirst(_tableFor(), filter); // ── 监听 ───────────────────────────────────────────────────────────────── @override - Stream> watchAll(TableInfo table) => - _core.repo.watchAll(table); + Stream> watchAll() => + _core.repo.watchAll(_tableFor()); @override - Stream> watchWhere( - TableInfo table, - Expression Function(T) filter, - ) => - _core.repo.watchWhere(table, filter); + Stream> watchWhere( + Expression Function(T) filter, + ) => + _core.repo.watchWhere(_tableFor(), filter); @override - Stream watchFirst( - TableInfo table, - Expression Function(T) filter, - ) => - _core.repo.watchFirst(table, filter); + Stream watchFirst( + Expression Function(T) filter, + ) => + _core.repo.watchFirst(_tableFor(), filter); // ── 原始 SQL ───────────────────────────────────────────────────────────── @@ -120,9 +120,8 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle { // ── 统计 ───────────────────────────────────────────────────────────────── @override - Future count( - TableInfo table, { + Future count({ Expression Function(T)? filter, }) => - _core.repo.count(table, filter: filter); -} + _core.repo.count(_tableFor(), filter: filter); +} \ No newline at end of file diff --git a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_wiring.dart b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_wiring.dart index 7f185f2..d8f6ab1 100644 --- a/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_wiring.dart +++ b/packages/storage_sdk/lib/src/presentation/wiring/storage_sdk_wiring.dart @@ -8,12 +8,15 @@ import 'storage_sdk_api_impl.dart'; /// SDK 依赖装配入口。 /// -/// 调用方传入数据库工厂,SDK 负责连接生命周期和 CRUD 机制。 +/// 调用方传入数据库工厂和表注册表,SDK 负责连接生命周期和 CRUD 机制。 /// /// 示例(im_app 的 DI 层): /// ```dart /// StorageSdkWiring.build( /// databaseFactory: (executor) => AppDatabase(executor), +/// tableRegistry: (db) => { +/// User: (db as AppDatabase).users, +/// }, /// ); /// ``` class StorageSdkWiring { @@ -21,10 +24,11 @@ class StorageSdkWiring { static StorageSdkApi build({ required GeneratedDatabase Function(QueryExecutor) databaseFactory, + required Map Function(GeneratedDatabase) tableRegistry, }) { final dataSource = DatabaseDataSource(databaseFactory: databaseFactory); final repo = DatabaseRepositoryImpl(dataSource); final core = StorageSdkCore(dataSource: dataSource, repo: repo); - return StorageSdkApiImpl(core: core); + return StorageSdkApiImpl(core: core, tableRegistry: tableRegistry); } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 78bd277..4d6e1b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ melos: gen: description: "Run build_runner build in all packages that use it" - run: melos exec --depends-on="build_runner" -- dart run build_runner build --delete-conflicting-outputs + run: bash scripts/table_gen.sh && melos exec --depends-on="build_runner" -- dart run build_runner build --delete-conflicting-outputs gen:watch: description: "Watch mode code generation in all packages that use build_runner" diff --git a/scripts/table_gen.sh b/scripts/table_gen.sh new file mode 100644 index 0000000..44bcf23 --- /dev/null +++ b/scripts/table_gen.sh @@ -0,0 +1,184 @@ +#!/bin/bash + +# 只更新 @DriftDatabase(tables: [...]), imports 和 getTableRegistry +# 其余内容保持不变 + +APP_DATABASE="apps/im_app/lib/data/local/drift/app_database.dart" + +# ── 扫描表类 ──────────────────────────────────────────────────────────────── +TABLE_CLASSES=() +DATA_CLASSES=() +GETTERS=() +IMPORTS=() + +# 项目根目录(脚本所在目录的上一层) +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# app_database.dart 所在目录(用于计算相对 import 路径) +APP_DATABASE_DIR="$(dirname "$ROOT_DIR/$APP_DATABASE")" + +echo "🔍 扫描所有包中的 drift 表文件..." +echo " 根目录: $ROOT_DIR" +echo " app_database 目录: $APP_DATABASE_DIR" + +# 搜索所有包中的 lib/data/local/drift/tables 目录下的 dart 文件 +while IFS= read -r FILE; do + echo " 📄 $FILE" + + # 提取继承 Table 或 View 的类名 + while IFS= read -r LINE; do + CLASS=$(echo "$LINE" | sed -E "s/.*class ([A-Za-z0-9]+) extends (Table|View).*/\1/") + + if [ -z "$CLASS" ]; then + continue + fi + + TABLE_CLASSES+=("$CLASS") + + # 提取 @DataClassName 注解 + DATA_CLASS=$(grep -E "@DataClassName\('([^']+)'\)" "$FILE" | sed -E "s/.*@DataClassName\('([^']+)'\).*/\1/" | head -1) + + if [ -z "$DATA_CLASS" ]; then + # 无注解时去掉末尾 's' 推断数据类名 + DATA_CLASS="${CLASS%s}" + fi + + DATA_CLASSES+=("$DATA_CLASS") + + # getter 名 = 表类名首字母小写(跟 drift 生成一致) + GETTER=$(echo "$CLASS" | awk '{print tolower(substr($0,1,1)) substr($0,2)}') + GETTERS+=("$GETTER") + + # 计算 package: import 路径(找到 pubspec.yaml 确定 package 名) + PKG_IMPORT=$(python3 -c " +import os, re + +file_path = '$FILE' +search = os.path.dirname(file_path) +pkg_name = None +pkg_root = None + +while search != '/': + pubspec = os.path.join(search, 'pubspec.yaml') + if os.path.exists(pubspec): + with open(pubspec) as f: + for line in f: + m = re.match(r'^name:\s*(\S+)', line) + if m: + pkg_name = m.group(1) + pkg_root = search + break + break + search = os.path.dirname(search) + +if pkg_name and pkg_root: + lib_path = os.path.join(pkg_root, 'lib') + rel = os.path.relpath(file_path, lib_path) + print(f'package:{pkg_name}/{rel}') +else: + db_dir = '$APP_DATABASE_DIR' + print(os.path.relpath(file_path, db_dir)) +") + IMPORTS+=("$PKG_IMPORT") + + echo " ✅ 表类: $CLASS → 数据类: $DATA_CLASS → getter: $GETTER" + echo " 📦 import: $PKG_IMPORT" + + done < <(grep -E "^class [A-Za-z0-9]+ extends (Table|View)" "$FILE") + +done < <(find "$ROOT_DIR" \ + -type f \ + -name "*.dart" \ + -path "*/lib/data/local/drift/tables/*" \ + ! -path "*/build/*" \ + ! -path "*/.dart_tool/*" \ + ! -path "*/.*") + +if [ ${#TABLE_CLASSES[@]} -eq 0 ]; then + echo "❌ 未找到任何 drift 表类,请检查搜索路径。" + exit 1 +fi + +echo "" +echo "✅ 共找到 ${#TABLE_CLASSES[@]} 个表类:" +for i in "${!TABLE_CLASSES[@]}"; do + echo " [${TABLE_CLASSES[$i]}] → [${DATA_CLASSES[$i]}] → getter: ${GETTERS[$i]} → ${IMPORTS[$i]}" +done + +TABLES_LIST=$(IFS=', '; echo "${TABLE_CLASSES[*]}") +echo "" +echo "📋 @DriftDatabase(tables: [$TABLES_LIST])" + +# ── 用 sed 替换 @DriftDatabase(tables: [...]) ────────────────────────────── +echo "" +echo "✍️ 更新 @DriftDatabase..." +sed -i '' "s/@DriftDatabase(tables: \[.*\])/@DriftDatabase(tables: [$TABLES_LIST])/" "$ROOT_DIR/$APP_DATABASE" +echo "✅ @DriftDatabase 已更新" + +# ── 生成 import 块和 registry ───────────────────────────────────────────── +IMPORT_BLOCK="" +for IMPORT_PATH in "${IMPORTS[@]}"; do + IMPORT_BLOCK+="import '$IMPORT_PATH';\n" +done + +REGISTRY="" +for i in "${!TABLE_CLASSES[@]}"; do + DATA="${DATA_CLASSES[$i]}" + GETTER="${GETTERS[$i]}" + REGISTRY+=" $DATA: database.$GETTER,\n" + echo " 注册: $DATA → database.$GETTER" +done + +echo "" +echo "✍️ 更新 imports + getTableRegistry..." + +python3 - "$ROOT_DIR/$APP_DATABASE" "$REGISTRY" "$IMPORT_BLOCK" << 'PYEOF' +import sys, re + +file_path = sys.argv[1] +registry = sys.argv[2].replace('\\n', '\n') +imports = sys.argv[3].replace('\\n', '\n') + +with open(file_path, 'r') as f: + content = f.read() + +# 替换 import 块(保留 drift import,替换其余 table imports) +new_content = re.sub( + r"(import 'package:drift/drift\.dart';\n)(?:import '[^']+\';\n)*", + "import 'package:drift/drift.dart';\n" + imports, + content +) + +# 替换 getTableRegistry 里的第二个 return { 块(不依赖注释) +parts = new_content.split('getTableRegistry', 1) +if len(parts) == 2: + after = parts[1] + # 跳过第一个 return {}(is! AppDatabase 那个),替换第二个 + count = [0] + def replace_second(m): + count[0] += 1 + if count[0] == 2: + return m.group(1) + '\n' + registry + ' ' + m.group(2) + return m.group(0) + new_after = re.sub( + r'(return \{)(.*?)(\};)', + lambda m: replace_second(re.match(r'(return \{)(.*?)(\};)', m.group(0), re.DOTALL)), + after, + flags=re.DOTALL + ) + # 用更简单直接的方式:找到所有 return { 块,替换第二个 + matches = list(re.finditer(r'return \{.*?\};', after, re.DOTALL)) + if len(matches) >= 2: + m = matches[1] + new_after = after[:m.start()] + 'return {\n' + registry + ' };' + after[m.end():] + new_content = parts[0] + 'getTableRegistry' + new_after + +with open(file_path, 'w') as f: + f.write(new_content) + +print("✅ imports + getTableRegistry 已更新") +PYEOF + +echo "" +echo "🎉 完成!下一步运行:" +echo " melos run gen"