|
|
@@ -21,12 +21,11 @@ import (
|
|
|
// ========== 数据结构 ==========
|
|
|
|
|
|
// 使用 types 包中的共享类型
|
|
|
-type DBConnection = types.DBConnection
|
|
|
type ConnectionGroup = types.ConnectionGroup
|
|
|
-type SQLScript = types.SQLScript
|
|
|
+type Script = types.Script
|
|
|
type ScriptGroup = types.ScriptGroup
|
|
|
|
|
|
-// StorageManager SQLite存储管理器
|
|
|
+// SQLite 存储管理器
|
|
|
type StorageManager struct {
|
|
|
dbPath string
|
|
|
db *sql.DB
|
|
|
@@ -72,7 +71,7 @@ func NewStorageManager(dbPath string) (*StorageManager, error) {
|
|
|
return sm, nil
|
|
|
}
|
|
|
|
|
|
-// initDatabase 初始化数据库表
|
|
|
+// 初始化数据库表
|
|
|
func (sm *StorageManager) initDatabase() error {
|
|
|
// 创建连接分组表
|
|
|
connectionGroupSQL := `
|
|
|
@@ -88,30 +87,52 @@ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
FOREIGN KEY (parent_id) REFERENCES connection_groups(id) ON DELETE CASCADE
|
|
|
);`
|
|
|
|
|
|
- // 创建连接表
|
|
|
+ // 创建连接表(基础通用信息,不包含 type/version)
|
|
|
connectionSQL := `
|
|
|
-CREATE TABLE IF NOT EXISTS connections (
|
|
|
-id TEXT PRIMARY KEY,
|
|
|
-group_id TEXT,
|
|
|
-name TEXT NOT NULL,
|
|
|
-description TEXT,
|
|
|
-db_type TEXT,
|
|
|
-db_version TEXT,
|
|
|
-server TEXT,
|
|
|
-port INTEGER,
|
|
|
-database TEXT,
|
|
|
-username TEXT,
|
|
|
-password_encrypted TEXT,
|
|
|
-use_ssh_tunnel BOOLEAN DEFAULT 0,
|
|
|
-connection_string TEXT,
|
|
|
-color TEXT,
|
|
|
-last_connected TIMESTAMP,
|
|
|
-auto_connect BOOLEAN DEFAULT 0,
|
|
|
-display_order INTEGER DEFAULT 0,
|
|
|
-created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
-updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
-FOREIGN KEY (group_id) REFERENCES connection_groups(id) ON DELETE SET NULL
|
|
|
-);`
|
|
|
+ CREATE TABLE IF NOT EXISTS connections (
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
+ group_id TEXT,
|
|
|
+ name TEXT NOT NULL,
|
|
|
+ description TEXT,
|
|
|
+ kind TEXT,
|
|
|
+ color TEXT,
|
|
|
+ auto_connect INTEGER DEFAULT 0,
|
|
|
+ display_order INTEGER DEFAULT 0,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ FOREIGN KEY (group_id) REFERENCES connection_groups(id) ON DELETE SET NULL
|
|
|
+ );`
|
|
|
+
|
|
|
+ // 专用表:db_connections
|
|
|
+ dbConnSQL := `
|
|
|
+ CREATE TABLE IF NOT EXISTS db_connections (
|
|
|
+ connection_id TEXT PRIMARY KEY REFERENCES connections(id) ON DELETE CASCADE,
|
|
|
+ type TEXT NOT NULL,
|
|
|
+ version TEXT,
|
|
|
+ server TEXT,
|
|
|
+ port INTEGER,
|
|
|
+ username TEXT,
|
|
|
+ password TEXT,
|
|
|
+ database_name TEXT,
|
|
|
+ connection_string TEXT,
|
|
|
+ use_ssh_tunnel INTEGER DEFAULT 0,
|
|
|
+ ssh_tunnel_connection_id TEXT,
|
|
|
+ last_connected TIMESTAMP
|
|
|
+ );`
|
|
|
+
|
|
|
+ // 专用表:server_connections
|
|
|
+ serverConnSQL := `
|
|
|
+ CREATE TABLE IF NOT EXISTS server_connections (
|
|
|
+ connection_id TEXT PRIMARY KEY REFERENCES connections(id) ON DELETE CASCADE,
|
|
|
+ type TEXT NOT NULL,
|
|
|
+ version TEXT,
|
|
|
+ server TEXT,
|
|
|
+ port INTEGER,
|
|
|
+ username TEXT,
|
|
|
+ auth_type TEXT,
|
|
|
+ private_key TEXT,
|
|
|
+ use_sudo INTEGER DEFAULT 0
|
|
|
+ );`
|
|
|
|
|
|
// 创建脚本分组表
|
|
|
scriptGroupSQL := `
|
|
|
@@ -125,9 +146,9 @@ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
FOREIGN KEY (parent_id) REFERENCES script_groups(id) ON DELETE CASCADE
|
|
|
);`
|
|
|
|
|
|
- // 创建SQL脚本表
|
|
|
- sqlScriptSQL := `
|
|
|
-CREATE TABLE IF NOT EXISTS sql_scripts (
|
|
|
+ // 创建脚本表(通用,scripts)
|
|
|
+ scriptsSQL := `
|
|
|
+CREATE TABLE IF NOT EXISTS scripts (
|
|
|
id TEXT PRIMARY KEY,
|
|
|
connection_id TEXT,
|
|
|
group_id TEXT,
|
|
|
@@ -135,7 +156,14 @@ name TEXT NOT NULL,
|
|
|
description TEXT,
|
|
|
content TEXT,
|
|
|
favorite BOOLEAN DEFAULT 0,
|
|
|
+language TEXT,
|
|
|
+metadata TEXT,
|
|
|
+tags TEXT,
|
|
|
+enabled INTEGER DEFAULT 1,
|
|
|
+owner TEXT,
|
|
|
+checksum TEXT,
|
|
|
last_executed TIMESTAMP,
|
|
|
+last_run_status TEXT,
|
|
|
execution_count INTEGER DEFAULT 0,
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
@@ -150,7 +178,7 @@ key TEXT PRIMARY KEY,
|
|
|
value TEXT
|
|
|
);`
|
|
|
|
|
|
- tables := []string{connectionGroupSQL, connectionSQL, scriptGroupSQL, sqlScriptSQL, configSQL}
|
|
|
+ tables := []string{connectionGroupSQL, connectionSQL, dbConnSQL, serverConnSQL, scriptGroupSQL, scriptsSQL, configSQL}
|
|
|
|
|
|
for i, sql := range tables {
|
|
|
if _, err := sm.db.Exec(sql); err != nil {
|
|
|
@@ -159,13 +187,17 @@ value TEXT
|
|
|
}
|
|
|
|
|
|
// 检查根连接分组是否已存在
|
|
|
- var count int
|
|
|
- err := sm.db.QueryRow("SELECT COUNT(*) FROM connection_groups WHERE id = ?", "root_default").Scan(&count)
|
|
|
+ var countNull sql.NullInt64
|
|
|
+ err := sm.db.QueryRow("SELECT COUNT(*) FROM connection_groups WHERE id = ?", "root_default").Scan(&countNull)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("检查根连接分组失败,错误: %v", err)
|
|
|
}
|
|
|
|
|
|
// 只有在根分组不存在时才创建
|
|
|
+ count := int64(0)
|
|
|
+ if countNull.Valid {
|
|
|
+ count = countNull.Int64
|
|
|
+ }
|
|
|
if count == 0 {
|
|
|
// 插入根连接分组
|
|
|
rootGroupSQL := `
|
|
|
@@ -187,17 +219,17 @@ VALUES (?, ?, ?, ?, ?)`
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// Close 关闭数据库连接
|
|
|
+// 关闭数据库连接
|
|
|
func (sm *StorageManager) Close() error {
|
|
|
return sm.db.Close()
|
|
|
}
|
|
|
|
|
|
-// generateID 生成唯一ID
|
|
|
+// 生成唯一 ID
|
|
|
func generateID() string {
|
|
|
return uuid.New().String()[:8]
|
|
|
}
|
|
|
|
|
|
-// generateGroupID 根据组名和随机值生成ID
|
|
|
+// 根据组名和随机值生成 ID
|
|
|
func generateGroupID(name string) string {
|
|
|
// 检查是否包含非拉丁字符(如中文字符)
|
|
|
hasNonLatin := false
|
|
|
@@ -261,7 +293,7 @@ func generateGroupID(name string) string {
|
|
|
return fmt.Sprintf("%s_%s", prefix, randomPart)
|
|
|
}
|
|
|
|
|
|
-// generateScriptID 根据分组ID和脚本名生成关联性ID
|
|
|
+// 根据分组 ID 和脚本名生成关联性 ID
|
|
|
func generateScriptID(groupID, name string) string {
|
|
|
// 从分组ID中提取前缀(分组ID格式为 prefix_random)
|
|
|
var groupPrefix string
|
|
|
@@ -331,19 +363,7 @@ func generateScriptID(groupID, name string) string {
|
|
|
return fmt.Sprintf("%s_%s_%s", groupPrefix, scriptPrefix, randomPart)
|
|
|
}
|
|
|
|
|
|
-// marshalTags 将标签数组序列化为JSON字符串
|
|
|
-func marshalTags(tags []string) (string, error) {
|
|
|
- if tags == nil {
|
|
|
- return "[]", nil
|
|
|
- }
|
|
|
- data, err := json.Marshal(tags)
|
|
|
- if err != nil {
|
|
|
- return "", err
|
|
|
- }
|
|
|
- return string(data), nil
|
|
|
-}
|
|
|
-
|
|
|
-// unmarshalTags 将JSON字符串反序列化为标签数组
|
|
|
+// 将 JSON 字符串反序列化为标签数组
|
|
|
func unmarshalTags(tagsStr string) ([]string, error) {
|
|
|
if tagsStr == "" {
|
|
|
return []string{}, nil
|
|
|
@@ -353,196 +373,342 @@ func unmarshalTags(tagsStr string) ([]string, error) {
|
|
|
return tags, err
|
|
|
}
|
|
|
|
|
|
-// CreateConnection 创建数据库连接
|
|
|
-func (sm *StorageManager) CreateConnection(groupID, name, description, dbType, dbVersion, server string, port int, username, password, database, connectionString string, useSSHTunnel bool, color string, autoConnect bool, displayOrder int) (*DBConnection, error) {
|
|
|
+// 创建数据库或服务器连接,返回聚合的连接详情
|
|
|
+func (sm *StorageManager) CreateConnection(groupID, name, description, kind, typ, version, server string, port int, username, password, database, connectionString string, useSSHTunnel bool, color string, autoConnect bool, displayOrder int) (*types.ConnectionWithDetails, error) {
|
|
|
sm.mu.Lock()
|
|
|
defer sm.mu.Unlock()
|
|
|
|
|
|
id := generateID()
|
|
|
now := time.Now()
|
|
|
|
|
|
- conn := &DBConnection{
|
|
|
- ID: id,
|
|
|
- GroupID: groupID,
|
|
|
- Name: name,
|
|
|
- Description: description,
|
|
|
- DBType: dbType,
|
|
|
- DBVersion: dbVersion,
|
|
|
- Server: server,
|
|
|
- Port: port,
|
|
|
- Username: username,
|
|
|
- Password: password,
|
|
|
- Database: database,
|
|
|
- ConnectionString: connectionString,
|
|
|
- UseSSHTunnel: useSSHTunnel,
|
|
|
- Color: color,
|
|
|
- LastConnected: now,
|
|
|
- AutoConnect: autoConnect,
|
|
|
- DisplayOrder: displayOrder,
|
|
|
- CreatedAt: now,
|
|
|
- UpdatedAt: now,
|
|
|
- }
|
|
|
-
|
|
|
- sql := `
|
|
|
-INSERT INTO connections (
|
|
|
-id, group_id, name, description, db_type, db_version,
|
|
|
-server, port, database, username, password_encrypted,
|
|
|
-use_ssh_tunnel, connection_string, color, last_connected,
|
|
|
-auto_connect, display_order, created_at, updated_at
|
|
|
-) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
-
|
|
|
- useSSHTunnelInt := 0
|
|
|
- if useSSHTunnel {
|
|
|
- useSSHTunnelInt = 1
|
|
|
+ tx, err := sm.db.Begin()
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("开始事务失败: %v", err)
|
|
|
}
|
|
|
+ defer func() {
|
|
|
+ if err != nil {
|
|
|
+ tx.Rollback()
|
|
|
+ }
|
|
|
+ }()
|
|
|
|
|
|
+ insertConnSQL := `INSERT INTO connections (id, group_id, name, description, kind, color, auto_connect, display_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
autoConnectInt := 0
|
|
|
if autoConnect {
|
|
|
autoConnectInt = 1
|
|
|
}
|
|
|
|
|
|
- _, execErr := sm.db.Exec(sql,
|
|
|
- id, groupID, name, description, dbType, dbVersion,
|
|
|
- server, port, database, username, password,
|
|
|
- useSSHTunnelInt, connectionString, color, now,
|
|
|
- autoConnectInt, displayOrder, now, now)
|
|
|
+ _, err = tx.Exec(insertConnSQL, id, groupID, name, description, kind, color, autoConnectInt, displayOrder, now, now)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("插入 connections 失败: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ var connDetail *types.DBConnectionDetail
|
|
|
+ var serverDetail *types.ServerConnectionDetail
|
|
|
|
|
|
- if execErr != nil {
|
|
|
- return nil, fmt.Errorf("创建数据库连接失败,连接名称: %s,数据库类型: %s,服务器: %s:%d,错误: %v", name, dbType, server, port, execErr)
|
|
|
+ if kind == "database" {
|
|
|
+ insertDBSQL := `INSERT INTO db_connections (connection_id, type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
+ useSSHTunnelInt := 0
|
|
|
+ if useSSHTunnel {
|
|
|
+ useSSHTunnelInt = 1
|
|
|
+ }
|
|
|
+ _, err = tx.Exec(insertDBSQL, id, typ, version, server, port, username, password, database, connectionString, useSSHTunnelInt, nil, now)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("插入 db_connections 失败: %v", err)
|
|
|
+ }
|
|
|
+ connDetail = &types.DBConnectionDetail{
|
|
|
+ ConnectionID: id,
|
|
|
+ Type: typ,
|
|
|
+ Version: version,
|
|
|
+ Server: server,
|
|
|
+ Port: port,
|
|
|
+ Username: username,
|
|
|
+ Password: password,
|
|
|
+ DatabaseName: database,
|
|
|
+ ConnectionString: connectionString,
|
|
|
+ UseSSHTunnel: useSSHTunnel,
|
|
|
+ SSHTunnelConnection: "",
|
|
|
+ LastConnected: now,
|
|
|
+ }
|
|
|
+ } else if kind == "server" {
|
|
|
+ insertServerSQL := `INSERT INTO server_connections (connection_id, type, version, server, port, username, auth_type, private_key, use_sudo) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
+ _, err = tx.Exec(insertServerSQL, id, typ, version, server, port, username, "", "", 0)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("插入 server_connections 失败: %v", err)
|
|
|
+ }
|
|
|
+ serverDetail = &types.ServerConnectionDetail{
|
|
|
+ ConnectionID: id,
|
|
|
+ Type: typ,
|
|
|
+ Version: version,
|
|
|
+ Server: server,
|
|
|
+ Port: port,
|
|
|
+ Username: username,
|
|
|
+ AuthType: "",
|
|
|
+ PrivateKey: "",
|
|
|
+ UseSudo: false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if err = tx.Commit(); err != nil {
|
|
|
+ return nil, fmt.Errorf("提交事务失败: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建返回对象
|
|
|
+ result := &types.ConnectionWithDetails{
|
|
|
+ Connection: types.Connection{
|
|
|
+ ID: id,
|
|
|
+ GroupID: groupID,
|
|
|
+ Name: name,
|
|
|
+ Description: description,
|
|
|
+ Kind: kind,
|
|
|
+ Color: color,
|
|
|
+ AutoConnect: autoConnect,
|
|
|
+ DisplayOrder: displayOrder,
|
|
|
+ CreatedAt: now,
|
|
|
+ UpdatedAt: now,
|
|
|
+ },
|
|
|
+ DBDetail: connDetail,
|
|
|
+ ServerDetail: serverDetail,
|
|
|
+ Scripts: []types.Script{},
|
|
|
}
|
|
|
|
|
|
- return conn, nil
|
|
|
+ return result, nil
|
|
|
}
|
|
|
|
|
|
-// GetConnection 获取连接信息
|
|
|
-func (sm *StorageManager) GetConnection(connID string) (*DBConnection, error) {
|
|
|
+// 获取连接信息并聚合详情
|
|
|
+func (sm *StorageManager) GetConnection(connID string) (*types.ConnectionWithDetails, error) {
|
|
|
sm.mu.RLock()
|
|
|
defer sm.mu.RUnlock()
|
|
|
|
|
|
- sql := `SELECT
|
|
|
-id, group_id, name, description, db_type, db_version,
|
|
|
-server, port, database, username, password_encrypted,
|
|
|
-use_ssh_tunnel, connection_string, color, last_connected,
|
|
|
-auto_connect, display_order, created_at, updated_at
|
|
|
-FROM connections WHERE id = ?`
|
|
|
-
|
|
|
- row := sm.db.QueryRow(sql, connID)
|
|
|
- conn := &DBConnection{}
|
|
|
-
|
|
|
- var lastConnected, createdAt, updatedAt []byte
|
|
|
- var useSSHTunnel, autoConnect int // SQLite以int形式存储布尔值
|
|
|
+ query := `SELECT id, group_id, name, description, kind, color, auto_connect, display_order, created_at, updated_at FROM connections WHERE id = ?`
|
|
|
+ row := sm.db.QueryRow(query, connID)
|
|
|
|
|
|
- err := row.Scan(
|
|
|
- &conn.ID, &conn.GroupID, &conn.Name, &conn.Description,
|
|
|
- &conn.DBType, &conn.DBVersion, &conn.Server, &conn.Port,
|
|
|
- &conn.Database, &conn.Username, &conn.Password,
|
|
|
- &useSSHTunnel, &conn.ConnectionString, &conn.Color, &lastConnected,
|
|
|
- &autoConnect, &conn.DisplayOrder, &createdAt, &updatedAt,
|
|
|
- )
|
|
|
+ var idNull, gidNull, nameNull, descriptionNull, kindNull, colorNull sql.NullString
|
|
|
+ var autoConnectNull, displayOrderNull sql.NullInt64
|
|
|
+ var createdAtNull, updatedAtNull sql.NullString
|
|
|
|
|
|
+ err := row.Scan(&idNull, &gidNull, &nameNull, &descriptionNull, &kindNull, &colorNull, &autoConnectNull, &displayOrderNull, &createdAtNull, &updatedAtNull)
|
|
|
if err != nil {
|
|
|
- if err.Error() == "sql: no rows in result set" {
|
|
|
+ if err == sql.ErrNoRows {
|
|
|
return nil, fmt.Errorf("未找到数据库连接,连接ID: %s", connID)
|
|
|
}
|
|
|
return nil, fmt.Errorf("查询数据库连接失败,连接ID: %s,错误: %v", connID, err)
|
|
|
}
|
|
|
|
|
|
- // 解析时间
|
|
|
- conn.LastConnected, _ = time.Parse("2006-01-02 15:04:05", string(lastConnected))
|
|
|
- conn.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", string(createdAt))
|
|
|
- conn.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", string(updatedAt))
|
|
|
+ // 将 sql.Null* 转换为常规类型(取不到值则使用零值)
|
|
|
+ id := ""
|
|
|
+ if idNull.Valid {
|
|
|
+ id = idNull.String
|
|
|
+ }
|
|
|
+ gid := ""
|
|
|
+ if gidNull.Valid {
|
|
|
+ gid = gidNull.String
|
|
|
+ }
|
|
|
+ name := ""
|
|
|
+ if nameNull.Valid {
|
|
|
+ name = nameNull.String
|
|
|
+ }
|
|
|
+ description := ""
|
|
|
+ if descriptionNull.Valid {
|
|
|
+ description = descriptionNull.String
|
|
|
+ }
|
|
|
+ kind := ""
|
|
|
+ if kindNull.Valid {
|
|
|
+ kind = kindNull.String
|
|
|
+ }
|
|
|
+ color := ""
|
|
|
+ if colorNull.Valid {
|
|
|
+ color = colorNull.String
|
|
|
+ }
|
|
|
|
|
|
- // 转换布尔值
|
|
|
- conn.UseSSHTunnel = useSSHTunnel != 0
|
|
|
- conn.AutoConnect = autoConnect != 0
|
|
|
+ autoConnectInt := 0
|
|
|
+ if autoConnectNull.Valid && autoConnectNull.Int64 != 0 {
|
|
|
+ autoConnectInt = 1
|
|
|
+ }
|
|
|
+ displayOrder := 0
|
|
|
+ if displayOrderNull.Valid {
|
|
|
+ displayOrder = int(displayOrderNull.Int64)
|
|
|
+ }
|
|
|
|
|
|
- return conn, nil
|
|
|
+ var conn types.ConnectionWithDetails
|
|
|
+ conn.Connection = types.Connection{
|
|
|
+ ID: id,
|
|
|
+ GroupID: gid,
|
|
|
+ Name: name,
|
|
|
+ Description: description,
|
|
|
+ Kind: kind,
|
|
|
+ Color: color,
|
|
|
+ AutoConnect: autoConnectInt != 0,
|
|
|
+ DisplayOrder: displayOrder,
|
|
|
+ }
|
|
|
+ if createdAtNull.Valid {
|
|
|
+ if t, err := time.Parse(time.RFC3339Nano, createdAtNull.String); err == nil {
|
|
|
+ conn.Connection.CreatedAt = t
|
|
|
+ } else if t2, err2 := time.Parse(time.RFC3339, createdAtNull.String); err2 == nil {
|
|
|
+ conn.Connection.CreatedAt = t2
|
|
|
+ } else if t3, err3 := time.Parse("2006-01-02 15:04:05", createdAtNull.String); err3 == nil {
|
|
|
+ conn.Connection.CreatedAt = t3
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if updatedAtNull.Valid {
|
|
|
+ if t, err := time.Parse(time.RFC3339Nano, updatedAtNull.String); err == nil {
|
|
|
+ conn.Connection.UpdatedAt = t
|
|
|
+ } else if t2, err2 := time.Parse(time.RFC3339, updatedAtNull.String); err2 == nil {
|
|
|
+ conn.Connection.UpdatedAt = t2
|
|
|
+ } else if t3, err3 := time.Parse("2006-01-02 15:04:05", updatedAtNull.String); err3 == nil {
|
|
|
+ conn.Connection.UpdatedAt = t3
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load detail according to explicit kind (no compatibility fallbacks)
|
|
|
+ if kind == "database" {
|
|
|
+ var detail types.DBConnectionDetail
|
|
|
+ var lastConnectedNull sql.NullString
|
|
|
+ var useSSHTunnelInt int
|
|
|
+ // 使用 sql.NullString 来接收可能为 NULL 的字符串列
|
|
|
+ var passwordNull sql.NullString
|
|
|
+ var connStrNull sql.NullString
|
|
|
+ var sshTunnelConnNull sql.NullString
|
|
|
+ err = sm.db.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, id).Scan(&detail.Type, &detail.Version, &detail.Server, &detail.Port, &detail.Username, &passwordNull, &detail.DatabaseName, &connStrNull, &useSSHTunnelInt, &sshTunnelConnNull, &lastConnectedNull)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("查询 db_connections 失败,连接ID: %s,错误: %v", id, err)
|
|
|
+ }
|
|
|
+ // 将 Nullable 字段转换为字符串(空值使用空字符串)
|
|
|
+ if passwordNull.Valid {
|
|
|
+ detail.Password = passwordNull.String
|
|
|
+ } else {
|
|
|
+ detail.Password = ""
|
|
|
+ }
|
|
|
+ if connStrNull.Valid {
|
|
|
+ detail.ConnectionString = connStrNull.String
|
|
|
+ } else {
|
|
|
+ detail.ConnectionString = ""
|
|
|
+ }
|
|
|
+ if sshTunnelConnNull.Valid {
|
|
|
+ detail.SSHTunnelConnection = sshTunnelConnNull.String
|
|
|
+ } else {
|
|
|
+ detail.SSHTunnelConnection = ""
|
|
|
+ }
|
|
|
+ detail.ConnectionID = id
|
|
|
+ detail.UseSSHTunnel = useSSHTunnelInt != 0
|
|
|
+ if lastConnectedNull.Valid {
|
|
|
+ if t, err := time.Parse(time.RFC3339Nano, lastConnectedNull.String); err == nil {
|
|
|
+ detail.LastConnected = t
|
|
|
+ } else if t2, err2 := time.Parse(time.RFC3339, lastConnectedNull.String); err2 == nil {
|
|
|
+ detail.LastConnected = t2
|
|
|
+ } else if t3, err3 := time.Parse("2006-01-02 15:04:05", lastConnectedNull.String); err3 == nil {
|
|
|
+ detail.LastConnected = t3
|
|
|
+ }
|
|
|
+ }
|
|
|
+ conn.DBDetail = &detail
|
|
|
+ } else if kind == "server" {
|
|
|
+ var sdetail types.ServerConnectionDetail
|
|
|
+ var useSudoInt int
|
|
|
+ // 使用 sql.NullString 来接收可能为 NULL 的字符串列
|
|
|
+ var authTypeNull sql.NullString
|
|
|
+ var privateKeyNull sql.NullString
|
|
|
+ err = sm.db.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, id).Scan(&sdetail.Type, &sdetail.Version, &sdetail.Server, &sdetail.Port, &sdetail.Username, &authTypeNull, &privateKeyNull, &useSudoInt)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("查询 server_connections 失败,连接ID: %s,错误: %v", id, err)
|
|
|
+ }
|
|
|
+ if authTypeNull.Valid {
|
|
|
+ sdetail.AuthType = authTypeNull.String
|
|
|
+ } else {
|
|
|
+ sdetail.AuthType = ""
|
|
|
+ }
|
|
|
+ if privateKeyNull.Valid {
|
|
|
+ sdetail.PrivateKey = privateKeyNull.String
|
|
|
+ } else {
|
|
|
+ sdetail.PrivateKey = ""
|
|
|
+ }
|
|
|
+ sdetail.ConnectionID = id
|
|
|
+ sdetail.UseSudo = useSudoInt != 0
|
|
|
+ conn.ServerDetail = &sdetail
|
|
|
+ }
|
|
|
+
|
|
|
+ // load scripts
|
|
|
+ scripts, err := sm.ListScripts(id)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("查询脚本失败,连接ID: %s,错误: %v", id, err)
|
|
|
+ }
|
|
|
+ conn.Scripts = scripts
|
|
|
+
|
|
|
+ return &conn, nil
|
|
|
}
|
|
|
|
|
|
-// ListConnections 获取所有连接
|
|
|
-func (sm *StorageManager) ListConnections() ([]*DBConnection, error) {
|
|
|
+// 获取所有连接并聚合详情
|
|
|
+func (sm *StorageManager) ListConnections() ([]types.ConnectionWithDetails, error) {
|
|
|
sm.mu.RLock()
|
|
|
defer sm.mu.RUnlock()
|
|
|
|
|
|
- sql := `SELECT
|
|
|
-id, group_id, name, description, db_type, db_version,
|
|
|
-server, port, database, username, password_encrypted,
|
|
|
-use_ssh_tunnel, connection_string, color, last_connected,
|
|
|
-auto_connect, display_order, created_at, updated_at
|
|
|
-FROM connections ORDER BY display_order, name`
|
|
|
-
|
|
|
- rows, err := sm.db.Query(sql)
|
|
|
+ rows, err := sm.db.Query(`SELECT id FROM connections ORDER BY display_order, name`)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("查询数据库连接列表失败,错误: %v", err)
|
|
|
}
|
|
|
defer rows.Close()
|
|
|
|
|
|
- var connections []*DBConnection
|
|
|
+ var results []types.ConnectionWithDetails
|
|
|
for rows.Next() {
|
|
|
- conn := &DBConnection{}
|
|
|
-
|
|
|
- var lastConnected, createdAt, updatedAt []byte
|
|
|
- var useSSHTunnel, autoConnect int
|
|
|
-
|
|
|
- err := rows.Scan(
|
|
|
- &conn.ID, &conn.GroupID, &conn.Name, &conn.Description,
|
|
|
- &conn.DBType, &conn.DBVersion, &conn.Server, &conn.Port,
|
|
|
- &conn.Database, &conn.Username, &conn.Password,
|
|
|
- &useSSHTunnel, &conn.ConnectionString, &conn.Color, &lastConnected,
|
|
|
- &autoConnect, &conn.DisplayOrder, &createdAt, &updatedAt,
|
|
|
- )
|
|
|
-
|
|
|
+ var idNull sql.NullString
|
|
|
+ if err := rows.Scan(&idNull); err != nil {
|
|
|
+ return nil, fmt.Errorf("扫描连接ID失败: %v", err)
|
|
|
+ }
|
|
|
+ id := ""
|
|
|
+ if idNull.Valid {
|
|
|
+ id = idNull.String
|
|
|
+ }
|
|
|
+ conn, err := sm.GetConnection(id)
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("解析数据库连接信息失败,错误: %v", err)
|
|
|
+ return nil, err
|
|
|
}
|
|
|
+ results = append(results, *conn)
|
|
|
+ }
|
|
|
|
|
|
- // 解析时间
|
|
|
- conn.LastConnected, _ = time.Parse("2006-01-02 15:04:05", string(lastConnected))
|
|
|
- conn.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", string(createdAt))
|
|
|
- conn.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", string(updatedAt))
|
|
|
-
|
|
|
- // 转换布尔值
|
|
|
- conn.UseSSHTunnel = useSSHTunnel != 0
|
|
|
- conn.AutoConnect = autoConnect != 0
|
|
|
-
|
|
|
- connections = append(connections, conn)
|
|
|
+ if err = rows.Err(); err != nil {
|
|
|
+ return nil, fmt.Errorf("遍历连接结果集失败: %v", err)
|
|
|
}
|
|
|
|
|
|
- return connections, nil
|
|
|
+ return results, nil
|
|
|
}
|
|
|
|
|
|
-// ListSQLScripts 获取连接相关的所有SQL脚本
|
|
|
-func (sm *StorageManager) ListSQLScripts(connID string) ([]SQLScript, error) {
|
|
|
+// 获取连接相关的所有脚本
|
|
|
+func (sm *StorageManager) ListScripts(connID string) ([]types.Script, error) {
|
|
|
sm.mu.RLock()
|
|
|
defer sm.mu.RUnlock()
|
|
|
|
|
|
- sql := `SELECT
|
|
|
+ query := `SELECT
|
|
|
id, connection_id, group_id, name, description, content, favorite,
|
|
|
- last_executed, execution_count, created_at, updated_at
|
|
|
- FROM sql_scripts WHERE connection_id = ? ORDER BY name`
|
|
|
+ language, metadata, tags, enabled, owner, checksum, last_executed, last_run_status, execution_count, created_at, updated_at
|
|
|
+ FROM scripts WHERE connection_id = ? ORDER BY name`
|
|
|
|
|
|
- rows, err := sm.db.Query(sql, connID)
|
|
|
+ rows, err := sm.db.Query(query, connID)
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("查询SQL脚本列表失败,连接ID: %s,错误: %v", connID, err)
|
|
|
+ return nil, fmt.Errorf("查询脚本列表失败,连接ID: %s,错误: %v", connID, err)
|
|
|
}
|
|
|
defer rows.Close()
|
|
|
|
|
|
- var scripts []SQLScript
|
|
|
+ var scripts []types.Script
|
|
|
for rows.Next() {
|
|
|
- var script SQLScript
|
|
|
+ var script types.Script
|
|
|
|
|
|
var lastExecuted, createdAt, updatedAt []byte
|
|
|
var favorite int
|
|
|
+ var language sql.NullString
|
|
|
+ var metadataStr sql.NullString
|
|
|
+ var tagsStr sql.NullString
|
|
|
+ var enabledInt sql.NullInt64
|
|
|
+ var owner sql.NullString
|
|
|
+ var checksum sql.NullString
|
|
|
+ var lastRunStatus sql.NullString
|
|
|
|
|
|
err := rows.Scan(
|
|
|
&script.ID, &script.ConnectionID, &script.GroupID,
|
|
|
&script.Name, &script.Description, &script.Content,
|
|
|
- &favorite, &lastExecuted, &script.ExecutionCount,
|
|
|
+ &favorite, &language, &metadataStr, &tagsStr, &enabledInt, &owner, &checksum, &lastExecuted, &lastRunStatus, &script.ExecutionCount,
|
|
|
&createdAt, &updatedAt,
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("解析SQL脚本信息失败,错误: %v", err)
|
|
|
+ return nil, fmt.Errorf("解析脚本信息失败,错误: %v", err)
|
|
|
}
|
|
|
|
|
|
// 解析时间
|
|
|
@@ -552,8 +718,39 @@ func (sm *StorageManager) ListSQLScripts(connID string) ([]SQLScript, error) {
|
|
|
script.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", string(createdAt))
|
|
|
script.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", string(updatedAt))
|
|
|
|
|
|
- // 转换布尔值
|
|
|
+ // 转换布尔/可空值
|
|
|
script.Favorite = favorite != 0
|
|
|
+ if language.Valid {
|
|
|
+ script.Language = language.String
|
|
|
+ }
|
|
|
+ if owner.Valid {
|
|
|
+ script.Owner = owner.String
|
|
|
+ }
|
|
|
+ if checksum.Valid {
|
|
|
+ script.Checksum = checksum.String
|
|
|
+ }
|
|
|
+ if lastRunStatus.Valid {
|
|
|
+ script.LastRunStatus = lastRunStatus.String
|
|
|
+ }
|
|
|
+ if enabledInt.Valid {
|
|
|
+ script.Enabled = enabledInt.Int64 != 0
|
|
|
+ }
|
|
|
+
|
|
|
+ // metadata
|
|
|
+ if metadataStr.Valid && metadataStr.String != "" {
|
|
|
+ var m map[string]string
|
|
|
+ if err := json.Unmarshal([]byte(metadataStr.String), &m); err == nil {
|
|
|
+ script.Metadata = m
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // tags
|
|
|
+ if tagsStr.Valid {
|
|
|
+ tags, err := unmarshalTags(tagsStr.String)
|
|
|
+ if err == nil {
|
|
|
+ script.Tags = tags
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
scripts = append(scripts, script)
|
|
|
}
|
|
|
@@ -561,6 +758,6 @@ func (sm *StorageManager) ListSQLScripts(connID string) ([]SQLScript, error) {
|
|
|
return scripts, nil
|
|
|
} // 注:其他方法已移至相应的文件中实现
|
|
|
// UpdateConnection, DeleteConnection, MoveConnection, GetAllConnections 在 connection_operations.go 中
|
|
|
-// CreateSQLScript, GetSQLScript, UpdateSQLScript, DeleteSQLScript, UpdateSQLScriptExecutionStats, ListSQLScripts 在 sql_scripts.go 中
|
|
|
+// CreateScript, GetScript, UpdateScript, DeleteScript, UpdateScriptExecutionStats, ListScripts 在 scripts.go 中
|
|
|
// CreateConnectionGroup, GetConnectionGroup, UpdateConnectionGroup, DeleteConnectionGroup, MoveConnectionGroup, GetRootConnectionGroup, GetConnectionGroupTree 在 connection_groups.go 中
|
|
|
// CreateScriptGroup, GetScriptGroup, UpdateScriptGroup, DeleteScriptGroup, MoveScriptGroup, GetRootScriptGroup, GetScriptGroupTree 在 script_groups.go 中
|