mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
8669879ad0
Introduce a one-time welcome dialog that explains CC Switch's workflow to new users: how their existing config is preserved as a "default" provider and how the bundled "Official" preset enables one-click revert. Upgrade users are excluded by checking is_providers_empty() at startup and never see the dialog. Persistence follows the existing *_confirmed convention in AppSettings (proxy/usage/stream_check/failover), stored in settings.json. The field is only written when the user explicitly clicks the confirm button, keeping its semantics strictly about user acknowledgement. Also adds two reusable DAO helpers: - Database::is_providers_empty for fresh-install detection, using EXISTS(SELECT 1) for a short-circuit query. - Database::get_bool_flag accepting "true" | "1", with init_default_official_providers migrated to use it. Dialog copy in zh/en/ja uses conditional phrasing so it stays accurate whether or not existing live config was found.
637 lines
24 KiB
Rust
637 lines
24 KiB
Rust
use crate::database::{lock_conn, Database};
|
|
use crate::error::AppError;
|
|
use crate::provider::{Provider, ProviderMeta};
|
|
use indexmap::IndexMap;
|
|
use rusqlite::params;
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
type OmoProviderRow = (
|
|
String,
|
|
String,
|
|
String,
|
|
Option<String>,
|
|
Option<i64>,
|
|
Option<usize>,
|
|
Option<String>,
|
|
String,
|
|
);
|
|
|
|
impl Database {
|
|
pub fn get_all_providers(
|
|
&self,
|
|
app_type: &str,
|
|
) -> Result<IndexMap<String, Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn.prepare(
|
|
"SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
|
FROM providers WHERE app_type = ?1
|
|
ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC"
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let provider_iter = stmt
|
|
.query_map(params![app_type], |row| {
|
|
let id: String = row.get(0)?;
|
|
let name: String = row.get(1)?;
|
|
let settings_config_str: String = row.get(2)?;
|
|
let website_url: Option<String> = row.get(3)?;
|
|
let category: Option<String> = row.get(4)?;
|
|
let created_at: Option<i64> = row.get(5)?;
|
|
let sort_index: Option<usize> = row.get(6)?;
|
|
let notes: Option<String> = row.get(7)?;
|
|
let icon: Option<String> = row.get(8)?;
|
|
let icon_color: Option<String> = row.get(9)?;
|
|
let meta_str: String = row.get(10)?;
|
|
let in_failover_queue: bool = row.get(11)?;
|
|
|
|
let settings_config =
|
|
serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
|
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
|
|
|
Ok((
|
|
id,
|
|
Provider {
|
|
id: "".to_string(), // Placeholder, set below
|
|
name,
|
|
settings_config,
|
|
website_url,
|
|
category,
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon,
|
|
icon_color,
|
|
in_failover_queue,
|
|
},
|
|
))
|
|
})
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut providers = IndexMap::new();
|
|
for provider_res in provider_iter {
|
|
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
|
provider.id = id.clone();
|
|
|
|
let mut stmt_endpoints = conn.prepare(
|
|
"SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC"
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let endpoints_iter = stmt_endpoints
|
|
.query_map(params![id, app_type], |row| {
|
|
let url: String = row.get(0)?;
|
|
let added_at: Option<i64> = row.get(1)?;
|
|
Ok((
|
|
url,
|
|
crate::settings::CustomEndpoint {
|
|
url: "".to_string(),
|
|
added_at: added_at.unwrap_or(0),
|
|
last_used: None,
|
|
},
|
|
))
|
|
})
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut custom_endpoints = HashMap::new();
|
|
for ep_res in endpoints_iter {
|
|
let (url, mut ep) = ep_res.map_err(|e| AppError::Database(e.to_string()))?;
|
|
ep.url = url.clone();
|
|
custom_endpoints.insert(url, ep);
|
|
}
|
|
|
|
if let Some(meta) = &mut provider.meta {
|
|
meta.custom_endpoints = custom_endpoints;
|
|
}
|
|
|
|
providers.insert(id, provider);
|
|
}
|
|
|
|
Ok(providers)
|
|
}
|
|
|
|
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn
|
|
.prepare("SELECT id FROM providers WHERE app_type = ?1 AND is_current = 1 LIMIT 1")
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut rows = stmt
|
|
.query(params![app_type])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
|
|
Ok(Some(
|
|
row.get(0).map_err(|e| AppError::Database(e.to_string()))?,
|
|
))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn get_provider_by_id(
|
|
&self,
|
|
id: &str,
|
|
app_type: &str,
|
|
) -> Result<Option<Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let result = conn.query_row(
|
|
"SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
|
FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
|row| {
|
|
let name: String = row.get(0)?;
|
|
let settings_config_str: String = row.get(1)?;
|
|
let website_url: Option<String> = row.get(2)?;
|
|
let category: Option<String> = row.get(3)?;
|
|
let created_at: Option<i64> = row.get(4)?;
|
|
let sort_index: Option<usize> = row.get(5)?;
|
|
let notes: Option<String> = row.get(6)?;
|
|
let icon: Option<String> = row.get(7)?;
|
|
let icon_color: Option<String> = row.get(8)?;
|
|
let meta_str: String = row.get(9)?;
|
|
let in_failover_queue: bool = row.get(10)?;
|
|
|
|
let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
|
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
|
|
|
Ok(Provider {
|
|
id: id.to_string(),
|
|
name,
|
|
settings_config,
|
|
website_url,
|
|
category,
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon,
|
|
icon_color,
|
|
in_failover_queue,
|
|
})
|
|
},
|
|
);
|
|
|
|
match result {
|
|
Ok(provider) => Ok(Some(provider)),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
Err(e) => Err(AppError::Database(e.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
|
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
|
|
|
let existing: Option<(bool, bool)> = tx
|
|
.query_row(
|
|
"SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![provider.id, app_type],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.ok();
|
|
|
|
let is_update = existing.is_some();
|
|
let (is_current, in_failover_queue) =
|
|
existing.unwrap_or((false, provider.in_failover_queue));
|
|
|
|
if is_update {
|
|
tx.execute(
|
|
"UPDATE providers SET
|
|
name = ?1,
|
|
settings_config = ?2,
|
|
website_url = ?3,
|
|
category = ?4,
|
|
created_at = ?5,
|
|
sort_index = ?6,
|
|
notes = ?7,
|
|
icon = ?8,
|
|
icon_color = ?9,
|
|
meta = ?10,
|
|
is_current = ?11,
|
|
in_failover_queue = ?12
|
|
WHERE id = ?13 AND app_type = ?14",
|
|
params![
|
|
provider.name,
|
|
serde_json::to_string(&provider.settings_config).map_err(|e| {
|
|
AppError::Database(format!("Failed to serialize settings_config: {e}"))
|
|
})?,
|
|
provider.website_url,
|
|
provider.category,
|
|
provider.created_at,
|
|
provider.sort_index,
|
|
provider.notes,
|
|
provider.icon,
|
|
provider.icon_color,
|
|
serde_json::to_string(&meta_clone).map_err(|e| AppError::Database(format!(
|
|
"Failed to serialize meta: {e}"
|
|
)))?,
|
|
is_current,
|
|
in_failover_queue,
|
|
provider.id,
|
|
app_type,
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
} else {
|
|
tx.execute(
|
|
"INSERT INTO providers (
|
|
id, app_type, name, settings_config, website_url, category,
|
|
created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue
|
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
|
params![
|
|
provider.id,
|
|
app_type,
|
|
provider.name,
|
|
serde_json::to_string(&provider.settings_config)
|
|
.map_err(|e| AppError::Database(format!("Failed to serialize settings_config: {e}")))?,
|
|
provider.website_url,
|
|
provider.category,
|
|
provider.created_at,
|
|
provider.sort_index,
|
|
provider.notes,
|
|
provider.icon,
|
|
provider.icon_color,
|
|
serde_json::to_string(&meta_clone)
|
|
.map_err(|e| AppError::Database(format!("Failed to serialize meta: {e}")))?,
|
|
is_current,
|
|
in_failover_queue,
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
for (url, endpoint) in endpoints {
|
|
tx.execute(
|
|
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
params![provider.id, app_type, url, endpoint.added_at],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
}
|
|
}
|
|
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"DELETE FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
|
params![app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_provider_settings_config(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
settings_config: &serde_json::Value,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"UPDATE providers SET settings_config = ?1 WHERE id = ?2 AND app_type = ?3",
|
|
params![
|
|
serde_json::to_string(settings_config).map_err(|e| AppError::Database(format!(
|
|
"Failed to serialize settings_config: {e}"
|
|
)))?,
|
|
provider_id,
|
|
app_type
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn add_custom_endpoint(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
url: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let added_at = chrono::Utc::now().timestamp_millis();
|
|
conn.execute(
|
|
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)",
|
|
params![provider_id, app_type, url, added_at],
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove_custom_endpoint(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
url: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"DELETE FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 AND url = ?3",
|
|
params![provider_id, app_type, url],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2",
|
|
params![app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
// OMO ↔ OMO Slim mutually exclusive: deactivate the opposite category
|
|
let opposite = match category {
|
|
"omo" => Some("omo-slim"),
|
|
"omo-slim" => Some("omo"),
|
|
_ => None,
|
|
};
|
|
if let Some(opp) = opposite {
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2",
|
|
params![app_type, opp],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
}
|
|
let updated = tx
|
|
.execute(
|
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
if updated != 1 {
|
|
return Err(AppError::Database(format!(
|
|
"Failed to set {category} provider current: provider '{provider_id}' not found in app '{app_type}'"
|
|
)));
|
|
}
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn is_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<bool, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
match conn.query_row(
|
|
"SELECT is_current FROM providers
|
|
WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
|row| row.get(0),
|
|
) {
|
|
Ok(is_current) => Ok(is_current),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
|
|
Err(e) => Err(AppError::Database(e.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn clear_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"UPDATE providers SET is_current = 0
|
|
WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_current_omo_provider(
|
|
&self,
|
|
app_type: &str,
|
|
category: &str,
|
|
) -> Result<Option<Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let row_data: Result<OmoProviderRow, rusqlite::Error> = conn.query_row(
|
|
"SELECT id, name, settings_config, category, created_at, sort_index, notes, meta
|
|
FROM providers
|
|
WHERE app_type = ?1 AND category = ?2 AND is_current = 1
|
|
LIMIT 1",
|
|
params![app_type, category],
|
|
|row| {
|
|
Ok((
|
|
row.get(0)?,
|
|
row.get(1)?,
|
|
row.get(2)?,
|
|
row.get(3)?,
|
|
row.get(4)?,
|
|
row.get(5)?,
|
|
row.get(6)?,
|
|
row.get(7)?,
|
|
))
|
|
},
|
|
);
|
|
|
|
let (id, name, settings_config_str, _row_category, created_at, sort_index, notes, meta_str) =
|
|
match row_data {
|
|
Ok(v) => v,
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
|
Err(e) => return Err(AppError::Database(e.to_string())),
|
|
};
|
|
|
|
let settings_config = serde_json::from_str(&settings_config_str).map_err(|e| {
|
|
AppError::Database(format!(
|
|
"Failed to parse {category} provider settings_config (provider_id={id}): {e}"
|
|
))
|
|
})?;
|
|
let meta: crate::provider::ProviderMeta = if meta_str.trim().is_empty() {
|
|
crate::provider::ProviderMeta::default()
|
|
} else {
|
|
serde_json::from_str(&meta_str).map_err(|e| {
|
|
AppError::Database(format!(
|
|
"Failed to parse {category} provider meta (provider_id={id}): {e}"
|
|
))
|
|
})?
|
|
};
|
|
|
|
Ok(Some(Provider {
|
|
id,
|
|
name,
|
|
settings_config,
|
|
website_url: None,
|
|
category: Some(category.to_string()),
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon: None,
|
|
icon_color: None,
|
|
in_failover_queue: false,
|
|
}))
|
|
}
|
|
|
|
/// 判断 providers 表是否为空(全 app_type 一起算)。
|
|
///
|
|
/// 用于区分"全新安装"和"升级用户":在启动流程 import/seed 之前调用。
|
|
/// 使用 `EXISTS` 短路查询,比 `COUNT(*)` 在将来表变大时更高效。
|
|
pub fn is_providers_empty(&self) -> Result<bool, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let exists: bool = conn
|
|
.query_row("SELECT EXISTS(SELECT 1 FROM providers)", [], |row| {
|
|
row.get(0)
|
|
})
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(!exists)
|
|
}
|
|
|
|
/// 仅获取指定 app 下所有 provider 的 id 集合。
|
|
///
|
|
/// 比 `get_all_providers` 轻量得多:只读 id 列、无 endpoint 子查询。
|
|
/// 用于只需要做存在性检查的场景(如 additive 模式的 live 同步去重)。
|
|
pub fn get_provider_ids(&self, app_type: &str) -> Result<HashSet<String>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn
|
|
.prepare("SELECT id FROM providers WHERE app_type = ?1")
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
let rows = stmt
|
|
.query_map(params![app_type], |row| row.get::<_, String>(0))
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
let mut ids = HashSet::new();
|
|
for row in rows {
|
|
ids.insert(row.map_err(|e| AppError::Database(e.to_string()))?);
|
|
}
|
|
Ok(ids)
|
|
}
|
|
|
|
/// 判断指定 app 下是否存在非官方种子的供应商。
|
|
///
|
|
/// 比 `get_all_providers` 轻量得多:只读 id 列、无 endpoint 子查询、首条命中即返回。
|
|
/// 用于 `import_default_config` 决定是否跳过 live 导入。
|
|
pub fn has_non_official_seed_provider(&self, app_type: &str) -> Result<bool, AppError> {
|
|
use crate::database::dao::providers_seed::is_official_seed_id;
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn
|
|
.prepare("SELECT id FROM providers WHERE app_type = ?1")
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
let mut rows = stmt
|
|
.query(params![app_type])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
while let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
|
|
let id: String = row.get(0).map_err(|e| AppError::Database(e.to_string()))?;
|
|
if !is_official_seed_id(&id) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
/// 计算指定 app 下一个可用的 sort_index(追加到末尾)。
|
|
fn next_sort_index_for_app(&self, app_type: &str) -> Result<usize, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let max: Option<i64> = conn
|
|
.query_row(
|
|
"SELECT MAX(sort_index) FROM providers WHERE app_type = ?1",
|
|
params![app_type],
|
|
|row| row.get(0),
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(max.map(|v| (v + 1) as usize).unwrap_or(0))
|
|
}
|
|
|
|
/// 启动时调用:补齐缺失的官方预设供应商(Claude / Codex / Gemini)。
|
|
///
|
|
/// 使用 settings flag `official_providers_seeded` 保证每个数据库只执行一次:
|
|
/// - 全新用户:seed 三条官方预设
|
|
/// - 老用户升级:同样会触发一次(flag 不存在),追加到末尾,不影响已有排序
|
|
/// - 用户删除 seed 后:不再重建(flag 已为 true),尊重用户意图
|
|
///
|
|
/// 与 `Database::save_provider` 的 UPSERT 语义配合,即使被意外重复调用
|
|
/// 也不会覆盖用户当前激活的供应商(is_current 字段会被保留)。
|
|
pub fn init_default_official_providers(&self) -> Result<usize, AppError> {
|
|
use crate::database::dao::providers_seed::OFFICIAL_SEEDS;
|
|
|
|
if self
|
|
.get_bool_flag("official_providers_seeded")
|
|
.unwrap_or(false)
|
|
{
|
|
return Ok(0);
|
|
}
|
|
|
|
let mut inserted = 0_usize;
|
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
|
|
|
for seed in OFFICIAL_SEEDS {
|
|
let app_type_str = seed.app_type.as_str();
|
|
|
|
// 若该 id 已存在(极端情况:用户曾手动用过同 id),跳过
|
|
if self.get_provider_by_id(seed.id, app_type_str)?.is_some() {
|
|
continue;
|
|
}
|
|
|
|
let next_sort_index = self.next_sort_index_for_app(app_type_str)?;
|
|
|
|
let settings_config: serde_json::Value =
|
|
serde_json::from_str(seed.settings_config_json).map_err(|e| {
|
|
AppError::Database(format!("Seed JSON parse failed for {}: {e}", seed.id))
|
|
})?;
|
|
|
|
let mut provider = Provider::with_id(
|
|
seed.id.to_string(),
|
|
seed.name.to_string(),
|
|
settings_config,
|
|
Some(seed.website_url.to_string()),
|
|
);
|
|
provider.category = Some("official".to_string());
|
|
provider.icon = Some(seed.icon.to_string());
|
|
provider.icon_color = Some(seed.icon_color.to_string());
|
|
provider.sort_index = Some(next_sort_index);
|
|
provider.created_at = Some(now_ms);
|
|
|
|
self.save_provider(app_type_str, &provider)?;
|
|
inserted += 1;
|
|
log::info!(
|
|
"✓ Seeded official provider: {} ({})",
|
|
seed.name,
|
|
app_type_str
|
|
);
|
|
}
|
|
|
|
// 即使 inserted=0(例如用户手动创建过同 id)也设置 flag 防止反复检查
|
|
self.set_setting("official_providers_seeded", "true")?;
|
|
|
|
Ok(inserted)
|
|
}
|
|
}
|