use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AppSettings { #[serde(default = "default_base_url")] pub base_url: String, /// 自定义形象图片 URL(通过 /api/media/upload 上传后本地记录) #[serde(default)] pub custom_mascot_url: Option, } impl Default for AppSettings { fn default() -> Self { Self { base_url: default_base_url(), custom_mascot_url: None } } } fn default_base_url() -> String { "https://cfc.cf-club.com".to_string() } pub struct SettingsStore { path: PathBuf, } impl SettingsStore { pub fn new(app_config_dir: PathBuf) -> Self { Self { path: app_config_dir.join("settings.json") } } pub fn load(&self) -> AppSettings { match fs::read_to_string(&self.path) { Ok(txt) => serde_json::from_str(&txt).unwrap_or_default(), Err(_) => AppSettings::default(), } } pub fn save(&self, s: &AppSettings) -> Result<(), String> { if let Some(dir) = self.path.parent() { fs::create_dir_all(dir).map_err(|e| e.to_string())?; } let txt = serde_json::to_string_pretty(s).map_err(|e| e.to_string())?; fs::write(&self.path, txt).map_err(|e| e.to_string()) } } #[cfg(test)] mod tests { use super::*; use std::fs; use std::sync::atomic::{AtomicUsize, Ordering}; static COUNTER: AtomicUsize = AtomicUsize::new(0); fn temp_dir() -> std::path::PathBuf { let n = COUNTER.fetch_add(1, Ordering::SeqCst); let dir = std::env::temp_dir().join(format!("xafpet-settings-test-{}", n)); let _ = fs::remove_dir_all(&dir); dir } #[test] fn default_when_file_missing() { let store = SettingsStore::new(temp_dir()); let cfg = store.load(); assert_eq!(cfg.base_url, "https://cfc.cf-club.com"); } #[test] fn default_when_file_malformed() { let dir = temp_dir(); fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("settings.json"), "not json").unwrap(); let store = SettingsStore::new(dir); let cfg = store.load(); assert_eq!(cfg.base_url, "https://cfc.cf-club.com"); } #[test] fn roundtrip_save_load() { let dir = temp_dir(); let store = SettingsStore::new(dir.clone()); let mut cfg = store.load(); cfg.base_url = "http://192.168.1.5:9082".into(); store.save(&cfg).unwrap(); let store2 = SettingsStore::new(dir.clone()); assert_eq!(store2.load().base_url, "http://192.168.1.5:9082"); let _ = fs::remove_dir_all(&dir); } #[test] fn custom_mascot_url_roundtrip() { let dir = temp_dir(); let store = SettingsStore::new(dir.clone()); let mut cfg = store.load(); assert!(cfg.custom_mascot_url.is_none()); cfg.custom_mascot_url = Some("https://cdn.example/x.png".into()); store.save(&cfg).unwrap(); let loaded = SettingsStore::new(dir.clone()).load(); assert_eq!(loaded.custom_mascot_url.as_deref(), Some("https://cdn.example/x.png")); let _ = fs::remove_dir_all(&dir); } }