dev-manager-tauri/src-tauri/src/commands/deploy_info.rs
lanrtop 9660bcbc48 feat: 服务器管理体系 + 云产品扩展 + 源码库 + API密钥管理
新增多个核心功能模块(后端 Rust 命令 + 前端 Panel 全部接线完成):

- 服务器注册表:servers.rs + ServerManagerPanel、ProjectServerPanel(项目关联服务器)
- SSH Config 自动同步:ssh_config.rs
- 服务器软件管理:server_software.rs + ServerSoftwareBlock
- 宿主机应用管理:host_apps.rs + HostAppsPanel
- 服务配置管理:service_config.rs + ServiceConfigBlock
- 项目部署信息:deploy_info.rs + DeployInfoPanel
- API密钥管理:api_keys.rs + ApiKeysPanel
- 云产品管理:cloud_products.rs + CloudProductsPanel(含 Docker应用、云数据库实例子模块)
- 源码库:source_library.rs + SourceLibraryPage(分类管理 + 项目详情)

导航栏新增「源码库」入口;蓝图 manifest 同步补录 7 个新模块
2026-04-17 22:25:06 +09:00

195 lines
6.6 KiB
Rust

use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DeployInfo {
pub id: String,
pub project_id: String,
pub server_host: Option<String>,
pub server_port: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub ssh_key_path: Option<String>,
pub quick_links: Option<String>, // JSON: [{label, url}]
pub notes: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SaveDeployInfoInput {
pub server_host: Option<String>,
pub server_port: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub ssh_key_path: Option<String>,
pub quick_links: Option<String>,
pub notes: Option<String>,
}
// ── 业务函数 ──────────────────────────────────────────────────────────────────
pub fn get_info(conn: &Connection, project_id: &str) -> Option<DeployInfo> {
conn.query_row(
"SELECT id, project_id, server_host, server_port, username, password,
ssh_key_path, quick_links, notes, updated_at
FROM project_deploy_info WHERE project_id = ?1",
params![project_id],
|row| {
Ok(DeployInfo {
id: row.get(0)?,
project_id: 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)?,
updated_at: row.get(9)?,
})
},
)
.ok()
}
pub fn save_info(
conn: &Connection,
project_id: &str,
input: &SaveDeployInfoInput,
) -> Result<DeployInfo, String> {
let existing = get_info(conn, project_id);
let id = existing
.as_ref()
.map(|e| e.id.clone())
.unwrap_or_else(|| format!("deploy-{}", uuid::Uuid::new_v4()));
conn.execute(
"INSERT INTO project_deploy_info
(id, project_id, server_host, server_port, username, password,
ssh_key_path, quick_links, notes, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, datetime('now'))
ON CONFLICT(project_id) DO UPDATE SET
server_host = ?3,
server_port = ?4,
username = ?5,
password = ?6,
ssh_key_path = ?7,
quick_links = ?8,
notes = ?9,
updated_at = datetime('now')",
params![
id,
project_id,
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_info(conn, project_id).ok_or_else(|| "保存后查询失败".to_string())
}
// ── Tauri command 薄包装层 ────────────────────────────────────────────────────
#[tauri::command]
pub fn get_deploy_info(project_id: String) -> Result<Option<DeployInfo>, String> {
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
Ok(get_info(&conn, &project_id))
}
#[tauri::command]
pub fn save_deploy_info(
project_id: String,
input: SaveDeployInfoInput,
) -> Result<DeployInfo, String> {
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
save_info(&conn, &project_id, &input)
}
// ── 单元测试 ──────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
fn setup() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE project_deploy_info (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL UNIQUE,
server_host TEXT,
server_port TEXT DEFAULT '22',
username TEXT,
password TEXT,
ssh_key_path TEXT,
quick_links TEXT,
notes TEXT,
updated_at TEXT DEFAULT (datetime('now'))
);",
)
.unwrap();
conn
}
#[test]
fn test_get_empty() {
let conn = setup();
assert!(get_info(&conn, "proj-1").is_none());
}
#[test]
fn test_save_and_get() {
let conn = setup();
let input = SaveDeployInfoInput {
server_host: Some("192.168.1.100".to_string()),
server_port: Some("22".to_string()),
username: Some("root".to_string()),
password: Some("secret".to_string()),
ssh_key_path: None,
quick_links: Some(r#"[{"label":"控制台","url":"https://example.com"}]"#.to_string()),
notes: Some("测试服务器".to_string()),
};
let info = save_info(&conn, "proj-1", &input).unwrap();
assert_eq!(info.server_host.as_deref(), Some("192.168.1.100"));
assert_eq!(info.username.as_deref(), Some("root"));
let fetched = get_info(&conn, "proj-1").unwrap();
assert_eq!(fetched.password.as_deref(), Some("secret"));
}
#[test]
fn test_upsert_overwrites() {
let conn = setup();
let input1 = SaveDeployInfoInput {
server_host: Some("1.1.1.1".to_string()),
server_port: Some("22".to_string()),
username: Some("admin".to_string()),
password: None,
ssh_key_path: None,
quick_links: None,
notes: None,
};
save_info(&conn, "proj-1", &input1).unwrap();
let input2 = SaveDeployInfoInput {
server_host: Some("2.2.2.2".to_string()),
server_port: Some("2222".to_string()),
username: Some("deploy".to_string()),
password: Some("new-pass".to_string()),
ssh_key_path: None,
quick_links: None,
notes: None,
};
let info = save_info(&conn, "proj-1", &input2).unwrap();
assert_eq!(info.server_host.as_deref(), Some("2.2.2.2"));
assert_eq!(info.username.as_deref(), Some("deploy"));
}
}