Files
nj/app/lib/data/models/sync_models.dart
iven 367f21de08
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
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
2026-06-03 17:20:51 +08:00

177 lines
4.5 KiB
Dart

// 同步协议模型 — 与 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,
);
}