feat(app): 统一同步协议 — SyncModels + ApiClient.sync + SyncEngine.tryBatchSync
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:
176
app/lib/data/models/sync_models.dart
Normal file
176
app/lib/data/models/sync_models.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
// 同步协议模型 — 与 Rust 端 SyncReq/SyncResp/SyncChange/ConflictInfo 一一对应
|
||||
//
|
||||
// 端点: POST /api/v1/diary/sync
|
||||
// Rust DTO: crates/erp-diary/src/dto.rs (SyncReq, SyncResp, SyncChange, ConflictInfo)
|
||||
|
||||
/// 同步请求 — 与 Rust SyncReq 对应
|
||||
///
|
||||
/// ```rust
|
||||
/// pub struct SyncReq {
|
||||
/// pub last_sync_time: Option<DateTime<Utc>>,
|
||||
/// pub changes: Vec<SyncChange>,
|
||||
/// }
|
||||
/// ```
|
||||
class SyncReq {
|
||||
final DateTime? lastSyncTime;
|
||||
final List<SyncChange> changes;
|
||||
|
||||
const SyncReq({this.lastSyncTime, this.changes = const []});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (lastSyncTime != null)
|
||||
'last_sync_time': lastSyncTime!.toUtc().toIso8601String(),
|
||||
'changes': changes.map((c) => c.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 同步变更条目 — 与 Rust SyncChange 枚举对应
|
||||
///
|
||||
/// ```rust
|
||||
/// pub enum SyncChange {
|
||||
/// CreateJournal { data: serde_json::Value },
|
||||
/// UpdateJournal { id: Uuid, version: i32, data: serde_json::Value },
|
||||
/// DeleteJournal { id: Uuid, version: i32 },
|
||||
/// }
|
||||
/// ```
|
||||
sealed class SyncChange {
|
||||
const SyncChange();
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
/// 从 JSON 反序列化
|
||||
factory SyncChange.fromJson(Map<String, dynamic> json) {
|
||||
if (json.containsKey('CreateJournal')) {
|
||||
return SyncChangeCreateJournal(
|
||||
data: json['CreateJournal']['data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
if (json.containsKey('UpdateJournal')) {
|
||||
final inner = json['UpdateJournal'] as Map<String, dynamic>;
|
||||
return SyncChangeUpdateJournal(
|
||||
id: inner['id'] as String,
|
||||
version: inner['version'] as int,
|
||||
data: inner['data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
if (json.containsKey('DeleteJournal')) {
|
||||
final inner = json['DeleteJournal'] as Map<String, dynamic>;
|
||||
return SyncChangeDeleteJournal(
|
||||
id: inner['id'] as String,
|
||||
version: inner['version'] as int,
|
||||
);
|
||||
}
|
||||
throw FormatException('Unknown SyncChange variant: $json');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建日记变更
|
||||
class SyncChangeCreateJournal extends SyncChange {
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const SyncChangeCreateJournal({required this.data});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'CreateJournal': {'data': data},
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新日记变更
|
||||
class SyncChangeUpdateJournal extends SyncChange {
|
||||
final String id;
|
||||
final int version;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const SyncChangeUpdateJournal({
|
||||
required this.id,
|
||||
required this.version,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'UpdateJournal': {
|
||||
'id': id,
|
||||
'version': version,
|
||||
'data': data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 删除日记变更
|
||||
class SyncChangeDeleteJournal extends SyncChange {
|
||||
final String id;
|
||||
final int version;
|
||||
|
||||
const SyncChangeDeleteJournal({
|
||||
required this.id,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'DeleteJournal': {
|
||||
'id': id,
|
||||
'version': version,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 同步响应 — 与 Rust SyncResp 对应
|
||||
///
|
||||
/// ```rust
|
||||
/// pub struct SyncResp {
|
||||
/// pub server_changes: Vec<serde_json::Value>,
|
||||
/// pub conflicts: Vec<ConflictInfo>,
|
||||
/// pub sync_time: DateTime<Utc>,
|
||||
/// }
|
||||
/// ```
|
||||
class SyncResp {
|
||||
final List<Map<String, dynamic>> serverChanges;
|
||||
final List<ConflictInfo> conflicts;
|
||||
final DateTime syncTime;
|
||||
|
||||
const SyncResp({
|
||||
required this.serverChanges,
|
||||
required this.conflicts,
|
||||
required this.syncTime,
|
||||
});
|
||||
|
||||
factory SyncResp.fromJson(Map<String, dynamic> json) => SyncResp(
|
||||
serverChanges: (json['server_changes'] as List)
|
||||
.map((e) => Map<String, dynamic>.from(e as Map))
|
||||
.toList(),
|
||||
conflicts: (json['conflicts'] as List)
|
||||
.map((e) => ConflictInfo.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
syncTime: DateTime.parse(json['sync_time'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// 冲突信息 — 与 Rust ConflictInfo 对应
|
||||
///
|
||||
/// ```rust
|
||||
/// pub struct ConflictInfo {
|
||||
/// pub journal_id: Uuid,
|
||||
/// pub local_version: i32,
|
||||
/// pub server_version: i32,
|
||||
/// }
|
||||
/// ```
|
||||
class ConflictInfo {
|
||||
final String journalId;
|
||||
final int localVersion;
|
||||
final int serverVersion;
|
||||
|
||||
const ConflictInfo({
|
||||
required this.journalId,
|
||||
required this.localVersion,
|
||||
required this.serverVersion,
|
||||
});
|
||||
|
||||
factory ConflictInfo.fromJson(Map<String, dynamic> json) => ConflictInfo(
|
||||
journalId: json['journal_id'] as String,
|
||||
localVersion: json['local_version'] as int,
|
||||
serverVersion: json['server_version'] as int,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user