feat(app): 统一同步协议 — SyncModels + ApiClient.sync + SyncEngine.tryBatchSync
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

Flutter ↔ Rust 同步协议对齐:
- 新增 sync_models.dart: SyncReq/SyncResp/SyncChange/ConflictInfo
  与 Rust dto.rs 一一对应 (CreateJournal/UpdateJournal/DeleteJournal)
- ApiClient.sync(): 调用 POST /diary/sync 批量同步端点
- SyncEngine.tryBatchSync(): PendingOperation → SyncChange 批量提交
  成功清空队列,冲突保留待用户处理

保留原有逐个同步 trySync() 作为降级方案
后端 509/509 测试通过, Flutter analyze 0 error
This commit is contained in:
iven
2026-06-03 17:20:51 +08:00
parent 1766cefde9
commit 367f21de08
3 changed files with 267 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ import 'package:isar/isar.dart';
import '../local/isar_database_native.dart';
import '../local/collections/pending_operation_collection.dart';
import '../models/sync_models.dart';
import '../remote/api_client.dart';
/// 同步操作类型
@@ -277,6 +278,79 @@ class SyncEngine {
await persistPendingQueue();
}
/// 批量同步 — 使用 POST /diary/sync 端点一次性提交所有变更
///
/// 将队列中的 PendingOperation 转换为 SyncChange 列表,
/// 调用 Rust sync_handler 批量处理,获取服务端变更和冲突。
/// 成功后清空队列;失败时保留队列供重试。
Future<SyncResp?> tryBatchSync({DateTime? lastSyncTime}) async {
if (_status == SyncStatus.syncing) return null;
if (_pendingQueue.isEmpty) {
_status = SyncStatus.idle;
return null;
}
_status = SyncStatus.syncing;
_lastError = null;
try {
// 转换: PendingOperation → SyncChange
final changes = _pendingQueue.map(_operationToSyncChange).toList();
final req = SyncReq(
lastSyncTime: lastSyncTime,
changes: changes,
);
final resp = await _apiClient.sync(req);
// 处理冲突 — 将冲突的操作保留在队列中
if (resp.conflicts.isNotEmpty) {
final conflictIds = resp.conflicts.map((c) => c.journalId).toSet();
// 移除已成功同步的非冲突操作,保留冲突操作
_pendingQueue.removeWhere(
(op) => !conflictIds.contains(op.id),
);
_lastError = '${resp.conflicts.length} 个操作存在版本冲突';
} else {
// 全部成功,清空队列
_pendingQueue.clear();
}
_status = _pendingQueue.isEmpty ? SyncStatus.idle : SyncStatus.paused;
await persistPendingQueue();
return resp;
} on OfflineException {
_status = SyncStatus.paused;
_lastError = '同步中断:网络不可用';
return null;
} catch (e) {
_status = SyncStatus.error;
_lastError = '批量同步失败: $e';
return null;
}
}
/// PendingOperation → SyncChange 转换
SyncChange _operationToSyncChange(PendingOperation op) {
switch (op.type) {
case SyncOperationType.create:
return SyncChangeCreateJournal(data: op.data);
case SyncOperationType.update:
return SyncChangeUpdateJournal(
id: op.id,
version: op.version,
data: op.data,
);
case SyncOperationType.delete:
return SyncChangeDeleteJournal(
id: op.id,
version: op.version,
);
}
}
/// 执行单个同步操作
Future<void> _executeOperation(PendingOperation operation) async {
switch (operation.type) {