feat(app): Isar 本地数据库集成 — Collection + Repository + 编辑器持久化 + SyncEngine 队列
新增文件: - data/local/collections/ 3 个 Isar Collection 定义 + 生成 Schema - data/repositories/isar_journal_repository.dart 完整 CRUD + 乐观锁 修改文件: - app.dart: IsarJournalRepository 注册为主 JournalRepository + SyncEngine 注入 - editor_page.dart: onSave 接入 JournalRepository,笔画/元素自动保存到 Isar - sync_engine.dart: 新增 persistPendingQueue/restorePendingQueue Isar 持久化 - isar_database.dart: 注册 3 个 Collection Schema - main.dart: 启动时初始化 Isar 架构: 离线优先 — Isar 为本地主仓库,Remote 供 SyncEngine 推送
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// 日记元素 Isar Collection — 本地持久化存储
|
||||
//
|
||||
// 与纯 Dart 模型 JournalElement 分离,通过转换函数桥接。
|
||||
// journalId 索引支持按日记查询所有元素。
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'journal_element_collection.g.dart';
|
||||
|
||||
@collection
|
||||
class JournalElementCollection {
|
||||
/// Isar 自增主键
|
||||
Id isarId = Isar.autoIncrement;
|
||||
|
||||
/// 业务 UUID(索引)
|
||||
@Index()
|
||||
String id = '';
|
||||
|
||||
/// 所属日记 ID(索引,用于外键查询)
|
||||
@Index()
|
||||
String journalId = '';
|
||||
|
||||
/// 元素类型(enum → string: text/image/sticker/handwriting_ref/tape)
|
||||
String elementType = 'text';
|
||||
|
||||
/// X 坐标
|
||||
double positionX = 0;
|
||||
|
||||
/// Y 坐标
|
||||
double positionY = 0;
|
||||
|
||||
/// 宽度
|
||||
double width = 100;
|
||||
|
||||
/// 高度
|
||||
double height = 100;
|
||||
|
||||
/// 旋转角度
|
||||
double rotation = 0;
|
||||
|
||||
/// 层级
|
||||
int zIndex = 0;
|
||||
|
||||
/// 结构化内容(JSON String)
|
||||
/// text: {'text':'...','fontSize':16.0}
|
||||
/// image: {'filePath':'...'}
|
||||
/// sticker: {'stickerPackId':'...','stickerId':'...'}
|
||||
/// handwriting_ref: {'strokesJson':'...','strokeCount':42}
|
||||
/// tape: {'tapeStyle':'washi_dots'}
|
||||
String contentJson = '{}';
|
||||
|
||||
/// 版本号(乐观锁)
|
||||
int version = 1;
|
||||
|
||||
/// 创建时间(epoch milliseconds)
|
||||
int createdAtEpoch = 0;
|
||||
|
||||
/// 更新时间(epoch milliseconds)
|
||||
int updatedAtEpoch = 0;
|
||||
|
||||
/// 软删除标记
|
||||
bool isDeleted = false;
|
||||
}
|
||||
2218
app/lib/data/local/collections/journal_element_collection.g.dart
Normal file
2218
app/lib/data/local/collections/journal_element_collection.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
60
app/lib/data/local/collections/journal_entry_collection.dart
Normal file
60
app/lib/data/local/collections/journal_entry_collection.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
// 日记条目 Isar Collection — 本地持久化存储
|
||||
//
|
||||
// 与纯 Dart 模型 JournalEntry 分离,通过转换函数桥接。
|
||||
// 业务 ID (String UUID) 作为索引字段,Isar 主键用 autoIncrement。
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'journal_entry_collection.g.dart';
|
||||
|
||||
@collection
|
||||
class JournalEntryCollection {
|
||||
/// Isar 自增主键
|
||||
Id isarId = Isar.autoIncrement;
|
||||
|
||||
/// 业务 UUID(索引,用于查找)
|
||||
@Index()
|
||||
String id = '';
|
||||
|
||||
/// 作者 ID
|
||||
String authorId = '';
|
||||
|
||||
/// 班级 ID(可选)
|
||||
String? classId;
|
||||
|
||||
/// 日记标题
|
||||
String title = '';
|
||||
|
||||
/// 日记日期(epoch milliseconds)
|
||||
int dateEpoch = 0;
|
||||
|
||||
/// 心情(enum → string)
|
||||
String mood = 'calm';
|
||||
|
||||
/// 天气(enum → string)
|
||||
String weather = 'sunny';
|
||||
|
||||
/// 标签列表(JSON String)
|
||||
String tagsJson = '[]';
|
||||
|
||||
/// 是否私密
|
||||
bool isPrivate = true;
|
||||
|
||||
/// 是否分享到班级
|
||||
bool sharedToClass = false;
|
||||
|
||||
/// 关联主题 ID(可选)
|
||||
String? assignedTopicId;
|
||||
|
||||
/// 版本号(乐观锁)
|
||||
int version = 1;
|
||||
|
||||
/// 创建时间(epoch milliseconds)
|
||||
int createdAtEpoch = 0;
|
||||
|
||||
/// 更新时间(epoch milliseconds)
|
||||
int updatedAtEpoch = 0;
|
||||
|
||||
/// 软删除标记
|
||||
bool isDeleted = false;
|
||||
}
|
||||
2502
app/lib/data/local/collections/journal_entry_collection.g.dart
Normal file
2502
app/lib/data/local/collections/journal_entry_collection.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// 待同步操作 Isar Collection — SyncEngine 队列持久化
|
||||
//
|
||||
// 应用退出时将内存队列写入 Isar,下次启动时恢复。
|
||||
// 保证离线操作不会因进程终止而丢失。
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'pending_operation_collection.g.dart';
|
||||
|
||||
@collection
|
||||
class PendingOperationCollection {
|
||||
/// Isar 自增主键
|
||||
Id isarId = Isar.autoIncrement;
|
||||
|
||||
/// 业务 UUID(索引)
|
||||
@Index()
|
||||
String id = '';
|
||||
|
||||
/// 操作类型:create / update / delete
|
||||
String operationType = 'create';
|
||||
|
||||
/// API 端点(如 '/diary/journals')
|
||||
String endpoint = '';
|
||||
|
||||
/// 请求负载(JSON String)
|
||||
String dataJson = '{}';
|
||||
|
||||
/// 资源版本号(乐观锁)
|
||||
int version = 1;
|
||||
|
||||
/// 创建时间(epoch milliseconds)
|
||||
int createdAtEpoch = 0;
|
||||
|
||||
/// 重试次数(最大 5 次)
|
||||
int retryCount = 0;
|
||||
}
|
||||
1408
app/lib/data/local/collections/pending_operation_collection.g.dart
Normal file
1408
app/lib/data/local/collections/pending_operation_collection.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,36 @@
|
||||
// Isar 数据库初始化 — 本地持久化存储
|
||||
//
|
||||
// Isar 3.x 要求 open() 时传入 List<CollectionSchema> 位置参数。
|
||||
// 由于我们使用手写不可变类而非 isar_generator 代码生成,
|
||||
// 需要在调用 [init] 时传入 schema 列表。
|
||||
// 当前阶段使用 [ensureInitialized] 占位,待后续添加 Isar Collection 后正式注册。
|
||||
// Isar 3.x 要求 open() 时传入 List<CollectionSchema>。
|
||||
// 通过 build_runner 生成 Schema,在 main.dart 启动时调用 init()。
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'collections/journal_entry_collection.dart';
|
||||
import 'collections/journal_element_collection.dart';
|
||||
import 'collections/pending_operation_collection.dart';
|
||||
|
||||
/// Isar 数据库单例管理
|
||||
///
|
||||
/// 使用方式(Phase 1 — 无 schema 时):
|
||||
/// ```dart
|
||||
/// // 直接使用,不初始化 Isar(内存仓库模式)
|
||||
/// ```
|
||||
///
|
||||
/// 使用方式(Phase 2 — 有 schema 后):
|
||||
/// ```dart
|
||||
/// final isar = await IsarDatabase.init(schemas: [JournalEntrySchema]);
|
||||
/// ```
|
||||
class IsarDatabase {
|
||||
IsarDatabase._();
|
||||
|
||||
static Isar? _instance;
|
||||
static bool _initialized = false;
|
||||
|
||||
/// 所有 Collection Schema(由 build_runner 生成)
|
||||
static final List<CollectionSchema<dynamic>> schemas = [
|
||||
JournalEntryCollectionSchema,
|
||||
JournalElementCollectionSchema,
|
||||
PendingOperationCollectionSchema,
|
||||
];
|
||||
|
||||
/// 是否已初始化
|
||||
static bool get isInitialized => _initialized;
|
||||
|
||||
/// 初始化数据库(需在 app 启动时调用,传入所有 CollectionSchema)
|
||||
/// 初始化数据库
|
||||
///
|
||||
/// - [schemas]: Isar Collection Schema 列表(由 isar_generator 生成)
|
||||
/// - 在应用文档目录下创建 isar 数据库文件
|
||||
/// - 开发模式开启 inspector(flutter pub global run isar_inspector)
|
||||
static Future<Isar> init({
|
||||
required List<CollectionSchema<dynamic>> schemas,
|
||||
}) async {
|
||||
/// 在 main() 中调用,open 之前需确保 WidgetsFlutterBinding 已初始化。
|
||||
static Future<Isar> init() async {
|
||||
if (_instance != null && _instance!.isOpen) return _instance!;
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
@@ -50,20 +45,14 @@ class IsarDatabase {
|
||||
}
|
||||
|
||||
/// 获取 Isar 实例(必须先调用 [init])
|
||||
///
|
||||
/// 如果未初始化会抛出 [StateError]。
|
||||
static Isar get instance {
|
||||
if (_instance == null || !_instance!.isOpen) {
|
||||
throw StateError(
|
||||
'IsarDatabase 未初始化,请先调用 IsarDatabase.init(schemas: [...])',
|
||||
);
|
||||
throw StateError('IsarDatabase 未初始化,请先调用 IsarDatabase.init()');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 关闭数据库连接
|
||||
///
|
||||
/// 通常只在应用退出时调用。
|
||||
static Future<void> close() async {
|
||||
if (_instance != null && _instance!.isOpen) {
|
||||
await _instance!.close();
|
||||
@@ -76,7 +65,7 @@ class IsarDatabase {
|
||||
static Future<void> clearAll() async {
|
||||
if (_instance == null || !_instance!.isOpen) return;
|
||||
await _instance!.writeTxn(() async {
|
||||
// TODO: 清空所有 collection
|
||||
await _instance!.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user