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:
@@ -1,20 +1,25 @@
|
||||
// 同步引擎 — WiFi 增量同步 + 操作队列
|
||||
// 同步引擎 — WiFi 增量同步 + 操作队列 + Isar 持久化
|
||||
//
|
||||
// 设计思路:
|
||||
// - 所有本地修改先入队 [PendingOperation]
|
||||
// - 网络恢复时自动批量同步
|
||||
// - 版本号冲突检测,Phase 1 使用"本地优先"策略
|
||||
// - 最大重试次数限制,超过后标记为冲突供用户手动解决
|
||||
// - 队列持久化到 Isar,应用退出后不丢失
|
||||
//
|
||||
// Phase 1 策略:本地优先
|
||||
// - 离线时正常使用,操作入队等待
|
||||
// - 联网后自动推送待同步操作
|
||||
// - 版本冲突时本地版本覆盖远端(简单策略)
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../local/isar_database.dart';
|
||||
import '../local/collections/pending_operation_collection.dart';
|
||||
import '../remote/api_client.dart';
|
||||
|
||||
/// 同步操作类型
|
||||
@@ -87,6 +92,9 @@ class PendingOperation {
|
||||
/// ```dart
|
||||
/// final engine = SyncEngine(apiClient: apiClient);
|
||||
///
|
||||
/// // 启动时恢复持久化队列
|
||||
/// await engine.restorePendingQueue();
|
||||
///
|
||||
/// // 本地修改后入队
|
||||
/// engine.enqueue(PendingOperation(
|
||||
/// id: 'op-1',
|
||||
@@ -99,6 +107,9 @@ class PendingOperation {
|
||||
///
|
||||
/// // 网络恢复时触发同步
|
||||
/// await engine.trySync();
|
||||
///
|
||||
/// // 应用退出时持久化
|
||||
/// await engine.persistPendingQueue();
|
||||
/// ```
|
||||
class SyncEngine {
|
||||
final ApiClient _apiClient;
|
||||
@@ -107,7 +118,7 @@ class SyncEngine {
|
||||
SyncStatus _status = SyncStatus.idle;
|
||||
String? _lastError;
|
||||
|
||||
SyncEngine({required this._apiClient});
|
||||
SyncEngine({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||
|
||||
/// 当前同步状态
|
||||
SyncStatus get status => _status;
|
||||
@@ -200,9 +211,10 @@ class SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// 全部同步完成
|
||||
// 全部同步完成,更新持久化
|
||||
_status = SyncStatus.idle;
|
||||
_lastError = null;
|
||||
await persistPendingQueue();
|
||||
}
|
||||
|
||||
/// 执行单个同步操作
|
||||
@@ -227,6 +239,100 @@ class SyncEngine {
|
||||
/// 获取当前队列中所有操作的快照(用于持久化到本地存储)
|
||||
///
|
||||
/// 应用退出时调用此方法,将待同步操作保存到 Isar,
|
||||
/// 下次启动时通过 [enqueueAll] 恢复。
|
||||
/// 下次启动时通过 [restorePendingQueue] 恢复。
|
||||
List<PendingOperation> get snapshot => _pendingQueue.toList();
|
||||
|
||||
// ============================================================
|
||||
// Isar 持久化
|
||||
// ============================================================
|
||||
|
||||
/// 将当前内存队列持久化到 Isar
|
||||
///
|
||||
/// 替换策略:先清空旧的持久化数据,再写入当前队列。
|
||||
/// 在 app 退出、isolate 暂停、或同步完成后调用。
|
||||
Future<void> persistPendingQueue() async {
|
||||
final isar = IsarDatabase.instance;
|
||||
final ops = snapshot;
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
// 清空旧数据
|
||||
await isar.pendingOperationCollections.clear();
|
||||
|
||||
// 写入当前队列
|
||||
for (final op in ops) {
|
||||
final col = _operationToCollection(op);
|
||||
await isar.pendingOperationCollections.put(col);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 从 Isar 恢复持久化队列到内存
|
||||
///
|
||||
/// 在 app 启动时调用,恢复上次退出时未同步的操作。
|
||||
Future<void> restorePendingQueue() async {
|
||||
final isar = IsarDatabase.instance;
|
||||
final persisted = await isar.pendingOperationCollections
|
||||
.where()
|
||||
.anyIsarId()
|
||||
.findAll();
|
||||
|
||||
for (final col in persisted) {
|
||||
final op = _collectionToOperation(col);
|
||||
_pendingQueue.add(op);
|
||||
}
|
||||
|
||||
if (_pendingQueue.isNotEmpty && _status == SyncStatus.idle) {
|
||||
_status = SyncStatus.paused;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 转换函数
|
||||
// ============================================================
|
||||
|
||||
/// PendingOperation → PendingOperationCollection
|
||||
PendingOperationCollection _operationToCollection(PendingOperation op) {
|
||||
return PendingOperationCollection()
|
||||
..id = op.id
|
||||
..operationType = op.type.httpMethod
|
||||
..endpoint = op.endpoint
|
||||
..dataJson = _encodeJson(op.data)
|
||||
..version = op.version
|
||||
..createdAtEpoch = op.createdAt.millisecondsSinceEpoch
|
||||
..retryCount = op.retryCount;
|
||||
}
|
||||
|
||||
/// PendingOperationCollection → PendingOperation
|
||||
PendingOperation _collectionToOperation(PendingOperationCollection col) {
|
||||
return PendingOperation(
|
||||
id: col.id,
|
||||
type: SyncOperationType.values.firstWhere(
|
||||
(t) => t.httpMethod == col.operationType,
|
||||
orElse: () => SyncOperationType.create,
|
||||
),
|
||||
endpoint: col.endpoint,
|
||||
data: _decodeJson(col.dataJson),
|
||||
version: col.version,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(col.createdAtEpoch),
|
||||
retryCount: col.retryCount,
|
||||
);
|
||||
}
|
||||
|
||||
/// 安全编码 JSON
|
||||
String _encodeJson(Map<String, dynamic> data) {
|
||||
try {
|
||||
return jsonEncode(data);
|
||||
} catch (_) {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全解码 JSON
|
||||
Map<String, dynamic> _decodeJson(String json) {
|
||||
try {
|
||||
return jsonDecode(json) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user