From 10eb07573c1456611503d9b9264bb1984e75afbe Mon Sep 17 00:00:00 2001 From: JSR Date: Thu, 19 Mar 2026 21:35:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=89=E5=8D=93=E7=AB=AFPo?= =?UTF-8?q?stgreSQL=E8=BF=9E=E6=8E=A5=E9=95=BF=E6=97=B6=E9=97=B4=E8=BD=AC?= =?UTF-8?q?=E5=9C=88=E6=97=A0=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/database.rs | 46 ++++++++++++----- src-tauri/src/db/connection.rs | 32 +++++++----- src-tauri/src/lib.rs | 82 +++++++++++++++++++++++------- src/components/OOBE/OOBE.tsx | 32 ++++++++++-- src/components/Settings.tsx | 48 +++++++++++++++-- 5 files changed, 191 insertions(+), 49 deletions(-) diff --git a/src-tauri/src/commands/database.rs b/src-tauri/src/commands/database.rs index 24c0cad..3e35dc8 100644 --- a/src-tauri/src/commands/database.rs +++ b/src-tauri/src/commands/database.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager, State}; +use tokio::time::{timeout, Duration}; use crate::db::connection::{create_postgres_connection, create_sqlite_connection}; use crate::db::connection::DatabaseType; @@ -15,6 +16,9 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use super::response::IpcResponse; +const DB_CONNECT_TIMEOUT_SECS: u64 = 15; +const DB_MIGRATION_TIMEOUT_SECS: u64 = 20; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestConnectionResult { pub success: bool, @@ -775,18 +779,28 @@ pub async fn db_test_connection( } else if connection_string.starts_with("postgres://") || connection_string.starts_with("postgresql://") { - match create_postgres_connection(&connection_string).await { - Ok(conn) => { + let connection_result = timeout( + Duration::from_secs(DB_CONNECT_TIMEOUT_SECS), + create_postgres_connection(&connection_string), + ) + .await; + + match connection_result { + Err(_) => TestConnectionResult { + success: false, + error: Some("PostgreSQL 连接超时,请检查网络或连接字符串".to_string()), + }, + Ok(Err(e)) => TestConnectionResult { + success: false, + error: Some(e.to_string()), + }, + Ok(Ok(conn)) => { let _ = conn.close().await; TestConnectionResult { success: true, error: None, } } - Err(e) => TestConnectionResult { - success: false, - error: Some(e.to_string()), - }, } } else { let path = connection_string.as_str(); @@ -819,12 +833,20 @@ pub async fn db_switch_connection( let (db_type, saved_connection_string, saved_status, conn) = if connection_string.starts_with("postgres://") || connection_string.starts_with("postgresql://") { - let conn = create_postgres_connection(&connection_string) - .await - .map_err(|e| e.to_string())?; - run_migration(&conn, DatabaseType::PostgreSQL) - .await - .map_err(|e| e.to_string())?; + let conn = timeout( + Duration::from_secs(DB_CONNECT_TIMEOUT_SECS), + create_postgres_connection(&connection_string), + ) + .await + .map_err(|_| "PostgreSQL 连接超时,请检查网络或连接字符串".to_string())? + .map_err(|e| e.to_string())?; + timeout( + Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS), + run_migration(&conn, DatabaseType::PostgreSQL), + ) + .await + .map_err(|_| "PostgreSQL 初始化超时,请稍后重试".to_string())? + .map_err(|e| e.to_string())?; ( "postgresql".to_string(), diff --git a/src-tauri/src/db/connection.rs b/src-tauri/src/db/connection.rs index 1f0e723..d54b43c 100644 --- a/src-tauri/src/db/connection.rs +++ b/src-tauri/src/db/connection.rs @@ -1,9 +1,26 @@ use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr}; +use std::time::Duration; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{info, warn}; +const DB_MAX_CONNECTIONS: u32 = 20; +const DB_MIN_CONNECTIONS: u32 = 1; +const DB_CONNECT_TIMEOUT_SECS: u64 = 12; +const DB_ACQUIRE_TIMEOUT_SECS: u64 = 12; +const DB_IDLE_TIMEOUT_SECS: u64 = 30; + +fn apply_default_connect_options(opt: &mut ConnectOptions) { + opt.max_connections(DB_MAX_CONNECTIONS) + .min_connections(DB_MIN_CONNECTIONS) + .connect_timeout(Duration::from_secs(DB_CONNECT_TIMEOUT_SECS)) + .acquire_timeout(Duration::from_secs(DB_ACQUIRE_TIMEOUT_SECS)) + .idle_timeout(Duration::from_secs(DB_IDLE_TIMEOUT_SECS)) + .sqlx_logging(true) + .sqlx_logging_level(tracing::log::LevelFilter::Info); +} + #[derive(Debug, Clone, PartialEq)] pub enum DatabaseType { SQLite, @@ -106,10 +123,7 @@ impl ConnectionManager { ); let mut opt = ConnectOptions::new(&url); - opt.max_connections(100) - .min_connections(5) - .sqlx_logging(true) - .sqlx_logging_level(tracing::log::LevelFilter::Info); + apply_default_connect_options(&mut opt); let conn = Database::connect(opt).await?; @@ -195,20 +209,14 @@ impl ConnectionManager { pub async fn create_sqlite_connection(path: &str) -> Result { let url = format!("sqlite://{}?mode=rwc", path); let mut opt = ConnectOptions::new(&url); - opt.max_connections(100) - .min_connections(5) - .sqlx_logging(true) - .sqlx_logging_level(tracing::log::LevelFilter::Info); + apply_default_connect_options(&mut opt); Database::connect(opt).await } pub async fn create_postgres_connection(url: &str) -> Result { let mut opt = ConnectOptions::new(url); - opt.max_connections(100) - .min_connections(5) - .sqlx_logging(true) - .sqlx_logging_level(tracing::log::LevelFilter::Info); + apply_default_connect_options(&mut opt); Database::connect(opt).await } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b4fd264..90d185f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ use crate::db::migration::run_migration; use crate::services::settings::{SettingsKey, SettingsValue}; use crate::{commands::*, state::AppState}; use tauri::{App, Manager}; +use tokio::time::{timeout, Duration}; #[cfg(desktop)] use tauri::{ image::Image, @@ -125,6 +126,9 @@ pub fn setup_app(app: &mut App) -> Result<(), Box> { } fn setup_database(app: &mut App) -> Result<(), Box> { + const DB_CONNECT_TIMEOUT_SECS: u64 = 15; + const DB_MIGRATION_TIMEOUT_SECS: u64 = 20; + let handle = app.handle().clone(); let db_path = if cfg!(all(debug_assertions, desktop)) { std::path::PathBuf::from("data.sql") @@ -166,9 +170,67 @@ fn setup_database(app: &mut App) -> Result<(), Box> { }; if !pg_connection_string.trim().is_empty() { - match create_postgres_connection(&pg_connection_string).await { - Ok(pg_conn) => { - if let Err(e) = run_migration(&pg_conn, DatabaseType::PostgreSQL).await { + match timeout( + Duration::from_secs(DB_CONNECT_TIMEOUT_SECS), + create_postgres_connection(&pg_connection_string), + ) + .await + { + Err(_) => { + let timeout_message = "PostgreSQL auto-connect timeout".to_string(); + eprintln!( + "PostgreSQL auto-connect timed out on startup, fallback to sqlite" + ); + settings + .set_value( + SettingsKey::PgConnectionStatus, + SettingsValue::Json(serde_json::json!({ + "connected": false, + "type": "sqlite", + "error": timeout_message + })), + ) + .await + .map_err(|err| format!("Failed to save pg status: {}", err))?; + } + Ok(Err(e)) => { + eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e); + settings + .set_value( + SettingsKey::PgConnectionStatus, + SettingsValue::Json(serde_json::json!({ + "connected": false, + "type": "sqlite", + "error": e.to_string() + })), + ) + .await + .map_err(|err| format!("Failed to save pg status: {}", err))?; + } + Ok(Ok(pg_conn)) => { + let migration_result = timeout( + Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS), + run_migration(&pg_conn, DatabaseType::PostgreSQL), + ) + .await; + + if let Err(_) = migration_result { + let timeout_message = "PostgreSQL migration timeout".to_string(); + eprintln!( + "PostgreSQL migration timed out on startup, fallback to sqlite" + ); + settings + .set_value( + SettingsKey::PgConnectionStatus, + SettingsValue::Json(serde_json::json!({ + "connected": false, + "type": "sqlite", + "error": timeout_message + })), + ) + .await + .map_err(|err| format!("Failed to save pg status: {}", err))?; + } else if let Ok(Err(e)) = migration_result { eprintln!("PostgreSQL migration failed on startup, fallback to sqlite: {}", e); settings .set_value( @@ -196,20 +258,6 @@ fn setup_database(app: &mut App) -> Result<(), Box> { eprintln!("Auto connected to PostgreSQL from saved connection string"); } } - Err(e) => { - eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e); - settings - .set_value( - SettingsKey::PgConnectionStatus, - SettingsValue::Json(serde_json::json!({ - "connected": false, - "type": "sqlite", - "error": e.to_string() - })), - ) - .await - .map_err(|err| format!("Failed to save pg status: {}", err))?; - } } } else { settings diff --git a/src/components/OOBE/OOBE.tsx b/src/components/OOBE/OOBE.tsx index dcf34d7..0a8a70e 100644 --- a/src/components/OOBE/OOBE.tsx +++ b/src/components/OOBE/OOBE.tsx @@ -34,6 +34,20 @@ interface reasonItem { const deepClone = (v: T): T => JSON.parse(JSON.stringify(v)) as T +const withTimeout = async (promise: Promise, ms: number, timeoutMessage: string): Promise => { + let timer: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(timeoutMessage)), ms) + }), + ]) + } finally { + if (timer) clearTimeout(timer) + } +} + const presetPrimaryColors = [ "#1677FF", "#2F54EB", @@ -363,18 +377,30 @@ export const OOBE: React.FC = ({ visible, onComplete }) => { setPgAutoLoading(true) try { if (!(window as any).api) throw new Error("api not ready") - const switchRes = await (window as any).api.dbSwitchConnection(connectionString) + const switchRes = await withTimeout( + (window as any).api.dbSwitchConnection(connectionString), + 25_000, + "连接 PostgreSQL 超时,请检查网络或连接字符串" + ) if (!switchRes?.success || switchRes?.data?.type !== "postgresql") { throw new Error(switchRes?.message || t("settings.database.switchFailed")) } - const previewRes = await (window as any).api.dbSyncPreview() + const previewRes = await withTimeout( + (window as any).api.dbSyncPreview(), + 25_000, + "同步预检查超时,请稍后重试" + ) if (!previewRes?.success || !previewRes?.data?.can_sync) { throw new Error(previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed")) } if (previewRes?.data?.need_sync) { - const syncApplyRes = await (window as any).api.dbSyncApply("keep_local") + const syncApplyRes = await withTimeout( + (window as any).api.dbSyncApply("keep_local"), + 45_000, + "执行同步超时,请稍后重试" + ) if (!syncApplyRes?.success || !syncApplyRes?.data?.success) { throw new Error( syncApplyRes?.data?.message || syncApplyRes?.message || t("settings.database.uploadFailed") diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 4f11313..b3400bd 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -27,6 +27,20 @@ type appSettings = { auto_score_enabled?: boolean } +const withTimeout = async (promise: Promise, ms: number, timeoutMessage: string): Promise => { + let timer: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(timeoutMessage)), ms) + }), + ]) + } finally { + if (timer) clearTimeout(timer) + } +} + export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => { const { t } = useTranslation() const [activeTab, setActiveTab] = useState("appearance") @@ -307,7 +321,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission } setPgTestLoading(true) try { - const res = await (window as any).api.dbTestConnection(pgConnectionString) + const res = await withTimeout( + (window as any).api.dbTestConnection(pgConnectionString), + 20_000, + "测试连接超时,请检查网络或连接字符串" + ) if (res.success && res.data?.success) { messageApi.success(t("settings.database.connectionTestSuccess")) } else { @@ -324,9 +342,17 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission const switchToPg = async () => { if (!(window as any).api) return + if (!pgConnectionString.trim()) { + messageApi.warning(t("settings.database.enterConnectionString")) + return + } setPgSwitchLoading(true) try { - const res = await (window as any).api.dbSwitchConnection(pgConnectionString) + const res = await withTimeout( + (window as any).api.dbSwitchConnection(pgConnectionString.trim()), + 25_000, + "切换 PostgreSQL 超时,请检查网络或连接字符串" + ) if (res.success) { messageApi.success( t("settings.database.switchedTo", { @@ -348,7 +374,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (!(window as any).api) return setPgSwitchLoading(true) try { - const res = await (window as any).api.dbSwitchConnection("") + const res = await withTimeout( + (window as any).api.dbSwitchConnection(""), + 15_000, + "切换 SQLite 超时,请稍后重试" + ) if (res.success) { messageApi.success(t("settings.database.switchedToSQLite")) setPgConnectionString("") @@ -372,7 +402,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission return } - const previewRes = await (window as any).api.dbSyncPreview() + const previewRes = await withTimeout( + (window as any).api.dbSyncPreview(), + 25_000, + "同步预检查超时,请稍后重试" + ) if (!previewRes.success || !previewRes.data?.can_sync) { messageApi.error(previewRes.message || previewRes.data?.message || t("settings.database.uploadFailed")) return @@ -394,7 +428,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission onOk: async () => { setPgUploadLoading(true) try { - const applyRes = await (window as any).api.dbSyncApply("keep_local") + const applyRes = await withTimeout( + (window as any).api.dbSyncApply("keep_local"), + 45_000, + "执行同步超时,请稍后重试" + ) if (applyRes.success && applyRes.data?.success) { messageApi.success( applyRes.data?.message || t("settings.database.uploadSuccess")