use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize}; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Server { pub id: String, pub name: String, pub server_host: String, pub server_port: Option, pub username: Option, pub password: Option, pub ssh_key_path: Option, pub quick_links: Option, pub notes: Option, pub created_at: Option, pub updated_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ProjectServerLink { pub project_id: String, pub server_id: String, pub role: Option, pub deploy_type: Option, pub deploy_path: Option, pub container_name: Option, pub start_command: Option, pub notes: Option, // 关联查询时附带的服务器信息 pub server_name: Option, pub server_host: Option, pub server_port: Option, pub username: Option, pub password: Option, pub ssh_key_path: Option, pub quick_links: Option, } #[derive(Debug, Deserialize)] pub struct SaveServerInput { pub name: String, pub server_host: String, pub server_port: Option, pub username: Option, pub password: Option, pub ssh_key_path: Option, pub quick_links: Option, pub notes: Option, } #[derive(Debug, Deserialize)] pub struct LinkServerInput { pub role: Option, pub deploy_type: Option, pub deploy_path: Option, pub container_name: Option, pub start_command: Option, pub notes: Option, } // ── 业务函数 ────────────────────────────────────────────────────────────────── pub fn list_servers(conn: &Connection) -> Vec { let mut stmt = match conn.prepare( "SELECT id, name, server_host, server_port, username, password, ssh_key_path, quick_links, notes, created_at, updated_at FROM servers ORDER BY updated_at DESC", ) { Ok(s) => s, Err(_) => return vec![], }; stmt.query_map([], |row| { Ok(Server { id: row.get(0)?, name: row.get(1)?, server_host: row.get(2)?, server_port: row.get(3)?, username: row.get(4)?, password: row.get(5)?, ssh_key_path: row.get(6)?, quick_links: row.get(7)?, notes: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, }) }) .map(|iter| iter.filter_map(|r| r.ok()).collect()) .unwrap_or_default() } pub fn create_server(conn: &Connection, input: &SaveServerInput) -> Result { let id = format!("srv-{}", uuid::Uuid::new_v4()); conn.execute( "INSERT INTO servers (id, name, server_host, server_port, username, password, ssh_key_path, quick_links, notes) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)", params![ id, input.name, input.server_host, input.server_port, input.username, input.password, input.ssh_key_path, input.quick_links, input.notes, ], ) .map_err(|e| e.to_string())?; get_server(conn, &id).ok_or_else(|| "创建后查询失败".into()) } pub fn get_server(conn: &Connection, id: &str) -> Option { conn.query_row( "SELECT id, name, server_host, server_port, username, password, ssh_key_path, quick_links, notes, created_at, updated_at FROM servers WHERE id = ?1", params![id], |row| { Ok(Server { id: row.get(0)?, name: row.get(1)?, server_host: row.get(2)?, server_port: row.get(3)?, username: row.get(4)?, password: row.get(5)?, ssh_key_path: row.get(6)?, quick_links: row.get(7)?, notes: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, }) }, ) .ok() } pub fn update_server(conn: &Connection, id: &str, input: &SaveServerInput) -> Result { let rows = conn.execute( "UPDATE servers SET name = ?1, server_host = ?2, server_port = ?3, username = ?4, password = ?5, ssh_key_path = ?6, quick_links = ?7, notes = ?8, updated_at = datetime('now') WHERE id = ?9", params![ input.name, input.server_host, input.server_port, input.username, input.password, input.ssh_key_path, input.quick_links, input.notes, id, ], ) .map_err(|e| e.to_string())?; if rows == 0 { return Err(format!("服务器 {} 不存在", id)); } get_server(conn, id).ok_or_else(|| "更新后查询失败".into()) } pub fn delete_server_fn(conn: &Connection, id: &str) -> Result<(), String> { conn.execute("DELETE FROM project_server_map WHERE server_id = ?1", params![id]) .map_err(|e| e.to_string())?; // 级联删除服务连接配置 + 宿主机应用 let _ = super::service_config::delete_configs_by_server(conn, id); let _ = super::host_apps::delete_apps_by_server(conn, id); conn.execute("DELETE FROM servers WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } // ── 关联管理 ────────────────────────────────────────────────────────────────── pub fn link_project_server( conn: &Connection, project_id: &str, server_id: &str, input: &LinkServerInput, ) -> Result<(), String> { conn.execute( "INSERT INTO project_server_map (project_id, server_id, role, deploy_type, deploy_path, container_name, start_command, notes) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(project_id, server_id) DO UPDATE SET role = ?3, deploy_type = ?4, deploy_path = ?5, container_name = ?6, start_command = ?7, notes = ?8", params![ project_id, server_id, input.role, input.deploy_type, input.deploy_path, input.container_name, input.start_command, input.notes, ], ) .map_err(|e| e.to_string())?; Ok(()) } pub fn unlink_project_server( conn: &Connection, project_id: &str, server_id: &str, ) -> Result<(), String> { conn.execute( "DELETE FROM project_server_map WHERE project_id = ?1 AND server_id = ?2", params![project_id, server_id], ) .map_err(|e| e.to_string())?; Ok(()) } const LINK_SELECT_SQL: &str = "SELECT m.project_id, m.server_id, m.role, m.deploy_type, m.deploy_path, m.container_name, m.start_command, m.notes, s.name, s.server_host, s.server_port, s.username, s.password, s.ssh_key_path, s.quick_links FROM project_server_map m JOIN servers s ON s.id = m.server_id"; fn link_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(ProjectServerLink { project_id: row.get(0)?, server_id: row.get(1)?, role: row.get(2)?, deploy_type: row.get(3)?, deploy_path: row.get(4)?, container_name: row.get(5)?, start_command: row.get(6)?, notes: row.get(7)?, server_name: row.get(8)?, server_host: row.get(9)?, server_port: row.get(10)?, username: row.get(11)?, password: row.get(12)?, ssh_key_path: row.get(13)?, quick_links: row.get(14)?, }) } /// 获取某个项目关联的所有服务器(含服务器详细信息) pub fn get_project_servers(conn: &Connection, project_id: &str) -> Vec { let sql = format!("{LINK_SELECT_SQL} WHERE m.project_id = ?1"); let mut stmt = match conn.prepare(&sql) { Ok(s) => s, Err(_) => return vec![], }; stmt.query_map(params![project_id], link_from_row) .map(|iter| iter.filter_map(|r| r.ok()).collect()) .unwrap_or_default() } /// 获取某台服务器关联的所有项目 pub fn get_server_projects(conn: &Connection, server_id: &str) -> Vec { let sql = format!("{LINK_SELECT_SQL} WHERE m.server_id = ?1"); let mut stmt = match conn.prepare(&sql) { Ok(s) => s, Err(_) => return vec![], }; stmt.query_map(params![server_id], link_from_row) .map(|iter| iter.filter_map(|r| r.ok()).collect()) .unwrap_or_default() } // ── Tauri command 薄包装层 ──────────────────────────────────────────────────── #[tauri::command] pub fn get_servers() -> Result, String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; Ok(list_servers(&conn)) } #[tauri::command] pub fn add_server(input: SaveServerInput) -> Result { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; create_server(&conn, &input) } #[tauri::command] pub fn edit_server(id: String, input: SaveServerInput) -> Result { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; update_server(&conn, &id, &input) } #[tauri::command] pub fn delete_server(id: String) -> Result<(), String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; delete_server_fn(&conn, &id) } #[tauri::command] pub fn link_server(project_id: String, server_id: String, input: LinkServerInput) -> Result<(), String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; link_project_server(&conn, &project_id, &server_id, &input) } #[tauri::command] pub fn unlink_server(project_id: String, server_id: String) -> Result<(), String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; unlink_project_server(&conn, &project_id, &server_id) } #[tauri::command] pub fn get_project_server_links(project_id: String) -> Result, String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; Ok(get_project_servers(&conn, &project_id)) } #[tauri::command] pub fn get_server_project_links(server_id: String) -> Result, String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; Ok(get_server_projects(&conn, &server_id)) } // ── 单元测试 ────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use rusqlite::Connection; fn setup() -> Connection { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch( "CREATE TABLE servers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, server_host TEXT NOT NULL, server_port TEXT DEFAULT '22', username TEXT, password TEXT, ssh_key_path TEXT, quick_links TEXT, notes TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE project_server_map ( project_id TEXT NOT NULL, server_id TEXT NOT NULL, role TEXT DEFAULT 'production', deploy_type TEXT DEFAULT 'bare', deploy_path TEXT, container_name TEXT, start_command TEXT, notes TEXT, PRIMARY KEY (project_id, server_id) );", ) .unwrap(); conn } #[test] fn test_crud_server() { let conn = setup(); let input = SaveServerInput { name: "测试服务器".into(), server_host: "192.168.1.100".into(), server_port: Some("22".into()), username: Some("root".into()), password: Some("secret".into()), ssh_key_path: None, quick_links: None, notes: None, }; let srv = create_server(&conn, &input).unwrap(); assert_eq!(srv.name, "测试服务器"); assert_eq!(srv.server_host, "192.168.1.100"); let all = list_servers(&conn); assert_eq!(all.len(), 1); let update = SaveServerInput { name: "生产服务器".into(), server_host: "10.0.0.1".into(), server_port: Some("2222".into()), username: Some("deploy".into()), password: None, ssh_key_path: None, quick_links: None, notes: Some("已更新".into()), }; let updated = update_server(&conn, &srv.id, &update).unwrap(); assert_eq!(updated.name, "生产服务器"); assert_eq!(updated.server_host, "10.0.0.1"); delete_server_fn(&conn, &srv.id).unwrap(); assert!(list_servers(&conn).is_empty()); } #[test] fn test_link_and_query() { let conn = setup(); let input = SaveServerInput { name: "Web Server".into(), server_host: "1.2.3.4".into(), server_port: Some("22".into()), username: Some("ubuntu".into()), password: None, ssh_key_path: None, quick_links: None, notes: None, }; let srv = create_server(&conn, &input).unwrap(); let link = LinkServerInput { role: Some("production".into()), deploy_type: None, deploy_path: Some("/var/www/app".into()), container_name: None, start_command: None, notes: None, }; link_project_server(&conn, "proj-1", &srv.id, &link).unwrap(); link_project_server(&conn, "proj-2", &srv.id, &LinkServerInput { role: Some("staging".into()), deploy_type: None, deploy_path: Some("/var/www/staging".into()), container_name: None, start_command: None, notes: None, }).unwrap(); let project_links = get_project_servers(&conn, "proj-1"); assert_eq!(project_links.len(), 1); assert_eq!(project_links[0].server_name.as_deref(), Some("Web Server")); assert_eq!(project_links[0].deploy_path.as_deref(), Some("/var/www/app")); let server_links = get_server_projects(&conn, &srv.id); assert_eq!(server_links.len(), 2); unlink_project_server(&conn, "proj-1", &srv.id).unwrap(); } #[test] fn test_delete_server_cascades_links() { let conn = setup(); let srv = create_server(&conn, &SaveServerInput { name: "Temp".into(), server_host: "1.1.1.1".into(), server_port: None, username: None, password: None, ssh_key_path: None, quick_links: None, notes: None, }).unwrap(); link_project_server(&conn, "proj-1", &srv.id, &LinkServerInput { role: None, deploy_type: None, deploy_path: None, container_name: None, start_command: None, notes: None, }).unwrap(); delete_server_fn(&conn, &srv.id).unwrap(); assert!(get_project_servers(&conn, "proj-1").is_empty()); } }