实现实时双写同步并接入核心写操作

This commit is contained in:
JSR
2026-03-19 18:04:41 +08:00
parent 141d073cc3
commit ec3929d65d
5 changed files with 199 additions and 138 deletions
+161 -121
View File
@@ -547,15 +547,15 @@ async fn ensure_student_tag_pair(
Ok(true)
}
async fn current_remote_and_local(
async fn current_remote_and_local_from_state(
app_handle: &AppHandle,
state: &State<'_, Arc<RwLock<AppState>>>,
app_state: &Arc<RwLock<AppState>>,
) -> Result<
Option<(sea_orm::DatabaseConnection, sea_orm::DatabaseConnection)>,
String,
> {
let (current_conn, db_type) = {
let state_guard = state.read();
let state_guard = app_state.read();
let db = state_guard.db.read().clone();
let settings = state_guard.settings.read();
let status_json = settings.get_value(SettingsKey::PgConnectionStatus);
@@ -588,6 +588,158 @@ async fn current_remote_and_local(
Ok(Some((local_conn, remote_conn)))
}
pub async fn realtime_dual_write_sync(app_state: &Arc<RwLock<AppState>>) -> Result<(), String> {
let app_handle = {
let state_guard = app_state.read();
state_guard.app_handle.clone()
};
let can_sync = current_remote_and_local_from_state(&app_handle, app_state)
.await?
.is_some();
if !can_sync {
return Ok(());
}
let result = db_sync_apply_internal(
ConflictStrategy::KeepRemote,
app_handle,
app_state.clone(),
)
.await?;
if !result.success {
return Err(result
.message
.unwrap_or_else(|| "实时双写同步失败".to_string()));
}
Ok(())
}
async fn db_sync_apply_internal(
strategy: ConflictStrategy,
app_handle: AppHandle,
app_state: Arc<RwLock<AppState>>,
) -> Result<DbSyncApplyResult, String> {
let Some((local_conn, remote_conn)) =
current_remote_and_local_from_state(&app_handle, &app_state).await?
else {
return Ok(DbSyncApplyResult {
success: false,
synced_records: 0,
resolved_conflicts: 0,
message: Some("当前不在 PostgreSQL 远程模式,无法执行同步".to_string()),
});
};
let local_students = load_students(&local_conn).await?;
let remote_students = load_students(&remote_conn).await?;
let local_reasons = load_reasons(&local_conn).await?;
let remote_reasons = load_reasons(&remote_conn).await?;
let local_tags = load_tags(&local_conn).await?;
let remote_tags = load_tags(&remote_conn).await?;
let local_events = load_events(&local_conn).await?;
let remote_events = load_events(&remote_conn).await?;
let local_pairs = load_student_tag_pairs(&local_conn).await?;
let remote_pairs = load_student_tag_pairs(&remote_conn).await?;
let (preferred, target) = if strategy == ConflictStrategy::KeepLocal {
(&local_conn, &remote_conn)
} else {
(&remote_conn, &local_conn)
};
let mut synced_records = 0usize;
let mut resolved_conflicts = 0usize;
for student in local_students.values() {
if upsert_student(&remote_conn, student).await? {
synced_records += 1;
}
}
for student in remote_students.values() {
if upsert_student(&local_conn, student).await? {
synced_records += 1;
}
}
for reason in local_reasons.values() {
if upsert_reason(&remote_conn, reason).await? {
synced_records += 1;
}
}
for reason in remote_reasons.values() {
if upsert_reason(&local_conn, reason).await? {
synced_records += 1;
}
}
for tag in local_tags.values() {
if upsert_tag(&remote_conn, tag).await? {
synced_records += 1;
}
}
for tag in remote_tags.values() {
if upsert_tag(&local_conn, tag).await? {
synced_records += 1;
}
}
for event in local_events.values() {
if upsert_event(&remote_conn, event).await? {
synced_records += 1;
}
}
for event in remote_events.values() {
if upsert_event(&local_conn, event).await? {
synced_records += 1;
}
}
for pair in local_pairs.union(&remote_pairs) {
if ensure_student_tag_pair(&local_conn, pair).await? {
synced_records += 1;
}
if ensure_student_tag_pair(&remote_conn, pair).await? {
synced_records += 1;
}
}
let preferred_students = load_students(preferred).await?;
for student in preferred_students.values() {
if upsert_student(target, student).await? {
resolved_conflicts += 1;
}
}
let preferred_reasons = load_reasons(preferred).await?;
for reason in preferred_reasons.values() {
if upsert_reason(target, reason).await? {
resolved_conflicts += 1;
}
}
let preferred_tags = load_tags(preferred).await?;
for tag in preferred_tags.values() {
if upsert_tag(target, tag).await? {
resolved_conflicts += 1;
}
}
let preferred_events = load_events(preferred).await?;
for event in preferred_events.values() {
if upsert_event(target, event).await? {
resolved_conflicts += 1;
}
}
let preferred_pairs = load_student_tag_pairs(preferred).await?;
for pair in &preferred_pairs {
if ensure_student_tag_pair(target, pair).await? {
resolved_conflicts += 1;
}
}
Ok(DbSyncApplyResult {
success: true,
synced_records,
resolved_conflicts,
message: Some("本地与远程同步完成".to_string()),
})
}
fn check_admin_permission(state: &Arc<RwLock<AppState>>) -> Result<(), String> {
let state_guard = state.read();
let mut permissions = state_guard.permissions.write();
@@ -795,7 +947,10 @@ pub async fn db_sync_preview(
) -> Result<IpcResponse<DbSyncPreviewResult>, String> {
check_admin_permission(&state)?;
let Some((local_conn, remote_conn)) = current_remote_and_local(&app_handle, &state).await? else {
let app_state = state.inner().clone();
let Some((local_conn, remote_conn)) =
current_remote_and_local_from_state(&app_handle, &app_state).await?
else {
return Ok(IpcResponse::success(DbSyncPreviewResult {
can_sync: false,
need_sync: false,
@@ -906,123 +1061,8 @@ pub async fn db_sync_apply(
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<DbSyncApplyResult>, String> {
check_admin_permission(&state)?;
let Some((local_conn, remote_conn)) = current_remote_and_local(&app_handle, &state).await? else {
return Ok(IpcResponse::success(DbSyncApplyResult {
success: false,
synced_records: 0,
resolved_conflicts: 0,
message: Some("当前不在 PostgreSQL 远程模式,无法执行同步".to_string()),
}));
};
let local_students = load_students(&local_conn).await?;
let remote_students = load_students(&remote_conn).await?;
let local_reasons = load_reasons(&local_conn).await?;
let remote_reasons = load_reasons(&remote_conn).await?;
let local_tags = load_tags(&local_conn).await?;
let remote_tags = load_tags(&remote_conn).await?;
let local_events = load_events(&local_conn).await?;
let remote_events = load_events(&remote_conn).await?;
let local_pairs = load_student_tag_pairs(&local_conn).await?;
let remote_pairs = load_student_tag_pairs(&remote_conn).await?;
let (preferred, target) = if strategy == ConflictStrategy::KeepLocal {
(&local_conn, &remote_conn)
} else {
(&remote_conn, &local_conn)
};
let mut synced_records = 0usize;
let mut resolved_conflicts = 0usize;
for student in local_students.values() {
if upsert_student(&remote_conn, student).await? {
synced_records += 1;
}
}
for student in remote_students.values() {
if upsert_student(&local_conn, student).await? {
synced_records += 1;
}
}
for reason in local_reasons.values() {
if upsert_reason(&remote_conn, reason).await? {
synced_records += 1;
}
}
for reason in remote_reasons.values() {
if upsert_reason(&local_conn, reason).await? {
synced_records += 1;
}
}
for tag in local_tags.values() {
if upsert_tag(&remote_conn, tag).await? {
synced_records += 1;
}
}
for tag in remote_tags.values() {
if upsert_tag(&local_conn, tag).await? {
synced_records += 1;
}
}
for event in local_events.values() {
if upsert_event(&remote_conn, event).await? {
synced_records += 1;
}
}
for event in remote_events.values() {
if upsert_event(&local_conn, event).await? {
synced_records += 1;
}
}
for pair in local_pairs.union(&remote_pairs) {
if ensure_student_tag_pair(&local_conn, pair).await? {
synced_records += 1;
}
if ensure_student_tag_pair(&remote_conn, pair).await? {
synced_records += 1;
}
}
let preferred_students = load_students(preferred).await?;
for student in preferred_students.values() {
if upsert_student(target, student).await? {
resolved_conflicts += 1;
}
}
let preferred_reasons = load_reasons(preferred).await?;
for reason in preferred_reasons.values() {
if upsert_reason(target, reason).await? {
resolved_conflicts += 1;
}
}
let preferred_tags = load_tags(preferred).await?;
for tag in preferred_tags.values() {
if upsert_tag(target, tag).await? {
resolved_conflicts += 1;
}
}
let preferred_events = load_events(preferred).await?;
for event in preferred_events.values() {
if upsert_event(target, event).await? {
resolved_conflicts += 1;
}
}
let preferred_pairs = load_student_tag_pairs(preferred).await?;
for pair in &preferred_pairs {
if ensure_student_tag_pair(target, pair).await? {
resolved_conflicts += 1;
}
}
Ok(IpcResponse::success(DbSyncApplyResult {
success: true,
synced_records,
resolved_conflicts,
message: Some("本地与远程同步完成".to_string()),
}))
let result = db_sync_apply_internal(strategy, app_handle, state.inner().clone()).await?;
Ok(IpcResponse::success(result))
}
#[tauri::command]
+3 -2
View File
@@ -13,6 +13,7 @@ use crate::db::entities::{score_events, students};
use crate::services::PermissionLevel;
use crate::state::AppState;
use super::database::realtime_dual_write_sync;
use super::response::IpcResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -172,7 +173,7 @@ pub async fn event_create(
active.update(&txn).await.map_err(|e| e.to_string())?;
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success(inserted.id))
}
Ok(None) => Ok(IpcResponse::error("Student not found")),
@@ -233,7 +234,7 @@ pub async fn event_delete(
.map_err(|e| e.to_string())?;
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
}
Ok(None) => Ok(IpcResponse::error("Event not found")),
+10 -2
View File
@@ -8,6 +8,7 @@ use crate::db::entities::reasons;
use crate::services::PermissionLevel;
use crate::state::AppState;
use super::database::realtime_dual_write_sync;
use super::response::IpcResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -117,7 +118,10 @@ pub async fn reason_create(
};
match new_reason.insert(conn).await {
Ok(inserted) => Ok(IpcResponse::success(inserted.id)),
Ok(inserted) => {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success(inserted.id))
}
Err(e) => Ok(IpcResponse::error(&format!(
"Failed to create reason: {}",
e
@@ -165,7 +169,10 @@ pub async fn reason_update(
}
match active.update(conn).await {
Ok(_) => Ok(IpcResponse::success_empty()),
Ok(_) => {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
}
Err(e) => Ok(IpcResponse::error(&format!(
"Failed to update reason: {}",
e
@@ -205,6 +212,7 @@ pub async fn reason_delete(
if result.rows_affected == 0 {
Ok(IpcResponse::error("记录不存在"))
} else {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success(DeleteResult {
changes: result.rows_affected as i32,
}))
+11 -3
View File
@@ -11,6 +11,7 @@ use crate::models::{StudentUpdate, StudentWithTags};
use crate::services::PermissionLevel;
use crate::state::AppState;
use super::database::realtime_dual_write_sync;
use super::response::{ImportResult, IpcResponse};
#[derive(Deserialize)]
@@ -126,7 +127,10 @@ pub async fn student_create(
};
match new_student.insert(conn).await {
Ok(inserted) => Ok(IpcResponse::success(inserted.id)),
Ok(inserted) => {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success(inserted.id))
}
Err(e) => Ok(IpcResponse::error(&format!(
"Failed to create student: {}",
e
@@ -181,7 +185,10 @@ pub async fn student_update(
}
match active.update(conn).await {
Ok(_) => Ok(IpcResponse::success_empty()),
Ok(_) => {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
}
Err(e) => Ok(IpcResponse::error(&format!(
"Failed to update student: {}",
e
@@ -230,7 +237,7 @@ pub async fn student_delete(
.map_err(|e| e.to_string())?;
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
}
Ok(None) => Ok(IpcResponse::error("Student not found")),
@@ -314,6 +321,7 @@ pub async fn student_import_from_xlsx(
}
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
}
Ok(IpcResponse::success(ImportResult {
+8 -4
View File
@@ -9,6 +9,7 @@ use crate::db::entities::{student_tags, tags};
use crate::services::PermissionLevel;
use crate::state::AppState;
use super::database::realtime_dual_write_sync;
use super::response::{IpcResponse, TagResponse};
fn check_admin_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
@@ -159,10 +160,13 @@ pub async fn tags_create(
};
match new_tag.insert(conn).await {
Ok(inserted) => Ok(IpcResponse::success(TagResponse {
Ok(inserted) => {
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success(TagResponse {
id: inserted.id,
name: inserted.name,
})),
}))
}
Err(e) => Ok(IpcResponse::error(&format!("Failed to create tag: {}", e))),
}
}
@@ -210,7 +214,7 @@ pub async fn tags_delete(
.map_err(|e| e.to_string())?;
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
}
Ok(None) => Ok(IpcResponse::error("Tag not found")),
@@ -264,7 +268,7 @@ pub async fn tags_update_student_tags(
}
txn.commit().await.map_err(|e| e.to_string())?;
realtime_dual_write_sync(state.inner()).await?;
Ok(IpcResponse::success_empty())
} else {
Ok(IpcResponse::error("Database not connected"))