From d62b9ca7bd8cebc07f143a438c5f2b045b112646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Fri, 13 Feb 2026 00:38:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=8F=AF=E8=A7=81=E6=80=A7=E8=BF=87=E6=BB=A4=E4=B8=8E?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=AE=A1=E7=90=86=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能: - 支持配置 MySQL/MongoDB 可见数据库列表 - 连接删除时自动清理关联数据并关闭连接池 - 新增加载数据库列表 API - 数据库错误提示优化 改进: - 代码简化:消除重复的表单验证和密码处理逻辑 - ResultPanel 表格高度计算重构 - 删除调试日志和临时文件 后端: - 新增 VisibleDatabases 字段到连接模型 - DeleteConnection 使用事务确保数据一致性 - LoadAllDatabases 支持 MySQL/MongoDB 数据库列表加载 --- app.go | 31 +- cmd/debug_db/main.go | 73 ++++ internal/api/connection_api.go | 97 +++-- internal/storage/connection_service.go | 161 +++++++- internal/storage/models/connection.go | 23 +- web/.gitignore | 31 ++ web/src/App.vue | 11 +- web/src/utils/database-error.ts | 114 ++++++ .../db-cli/components/ConnectionForm.vue | 387 ++++++++++++++---- .../db-cli/components/ConnectionTree.vue | 102 +++-- .../views/db-cli/components/ResultPanel.vue | 307 +++++--------- web/src/views/db-cli/index.vue | 6 +- web/src/wailsjs/wailsjs/go/main/App.d.ts | 4 +- web/src/wailsjs/wailsjs/go/main/App.js | 4 + web/src/wailsjs/wailsjs/go/models.ts | 28 ++ 15 files changed, 993 insertions(+), 386 deletions(-) create mode 100644 cmd/debug_db/main.go create mode 100644 web/.gitignore create mode 100644 web/src/utils/database-error.ts diff --git a/app.go b/app.go index 9745cbb..305cf5d 100644 --- a/app.go +++ b/app.go @@ -6,8 +6,8 @@ import ( "net/http" "os" "path/filepath" - "strings" stdruntime "runtime" + "strings" "time" "u-desk/internal/api" @@ -23,15 +23,15 @@ import ( // App 应用结构体 type App struct { - ctx context.Context - db *database.DB - connectionAPI *api.ConnectionAPI - sqlAPI *api.SqlAPI - tabAPI *api.TabAPI - updateAPI *api.UpdateAPI - configAPI *api.ConfigAPI - fileServer *http.Server - filesystem *filesystem.FileSystemService + ctx context.Context + db *database.DB + connectionAPI *api.ConnectionAPI + sqlAPI *api.SqlAPI + tabAPI *api.TabAPI + updateAPI *api.UpdateAPI + configAPI *api.ConfigAPI + fileServer *http.Server + filesystem *filesystem.FileSystemService } // NewApp 创建新的应用实例 @@ -362,9 +362,9 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) { if err != nil { // 目标文件不存在或无法访问 return map[string]interface{}{ - "success": true, - "targetPath": targetPath, - "targetExists": false, + "success": true, + "targetPath": targetPath, + "targetExists": false, "targetAccessible": false, }, nil } @@ -434,6 +434,11 @@ func (a *App) TestDbConnectionWithParams(req api.TestConnectionRequest) error { return a.connectionAPI.TestDbConnectionWithParams(req) } +// LoadAllDatabases 加载全部数据库列表 +func (a *App) LoadAllDatabases(req api.LoadAllDatabasesRequest) ([]string, error) { + return a.connectionAPI.LoadAllDatabases(req) +} + // ExecuteSQL 执行 SQL 语句 // 注意:SQL 语句应该已经包含分页信息(LIMIT 和 OFFSET),由客户端添加 func (a *App) ExecuteSQL(connectionId uint, sqlStr string, database string) (map[string]interface{}, error) { diff --git a/cmd/debug_db/main.go b/cmd/debug_db/main.go new file mode 100644 index 0000000..15f7cdf --- /dev/null +++ b/cmd/debug_db/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "log" + + "u-desk/internal/storage" + "u-desk/internal/storage/models" +) + +func main() { + // 初始化数据库 + db, err := storage.Init() + if err != nil { + log.Fatalf("数据库初始化失败: %v", err) + } + + fmt.Println("=== 数据库连接配置调试工具 ===") + fmt.Println() + + // 列出所有连接 + var connections []models.DbConnection + result := db.Order("id").Find(&connections) + if result.Error != nil { + log.Fatalf("查询失败: %v", result.Error) + } + + fmt.Printf("当前有 %d 个连接配置:\n", len(connections)) + fmt.Println() + + for _, conn := range connections { + fmt.Printf("ID: %d\n", conn.ID) + fmt.Printf(" 名称: %s\n", conn.Name) + fmt.Printf(" 类型: %s\n", conn.Type) + fmt.Printf(" 主机: %s:%d\n", conn.Host, conn.Port) + fmt.Printf(" 用户名: %s\n", conn.Username) + fmt.Printf(" 创建时间: %s\n", conn.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Println() + } + + // 询问用户操作 + var choice int + fmt.Print("请选择操作:\n") + fmt.Print("1. 删除指定 ID 的连接\n") + fmt.Print("2. 列出连接详情\n") + fmt.Print("0. 退出\n") + fmt.Print("请输入: ") + fmt.Scanln(&choice) + + if choice == 1 { + var id uint + fmt.Print("请输入要删除的连接 ID: ") + fmt.Scanln(&id) + + // 确认 + var confirm string + fmt.Printf("确认删除 ID=%d 的连接吗?(y/N): ", id) + fmt.Scanln(&confirm) + + if confirm == "y" || confirm == "Y" { + result := db.Delete(&models.DbConnection{}, id) + if result.Error != nil { + log.Printf("删除失败: %v", result.Error) + } else { + fmt.Printf("删除成功!影响行数: %d\n", result.RowsAffected) + } + } else { + fmt.Println("已取消删除") + } + } + + fmt.Println("\n工具退出") +} diff --git a/internal/api/connection_api.go b/internal/api/connection_api.go index f9d88f8..fbd33cd 100644 --- a/internal/api/connection_api.go +++ b/internal/api/connection_api.go @@ -1,18 +1,18 @@ package api import ( - "u-desk/internal/service" + "u-desk/internal/storage" "u-desk/internal/storage/models" ) // ConnectionAPI 连接管理API type ConnectionAPI struct { - connService *service.ConnectionService + connService *storage.ConnectionService } // NewConnectionAPI 创建连接管理API func NewConnectionAPI() (*ConnectionAPI, error) { - connService, err := service.NewConnectionService() + connService, err := storage.NewConnectionService() if err != nil { return nil, err } @@ -21,29 +21,31 @@ func NewConnectionAPI() (*ConnectionAPI, error) { // SaveConnectionRequest 保存连接请求结构体 type SaveConnectionRequest struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Host string `json:"host"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - Database string `json:"database"` - Options string `json:"options"` + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + Database string `json:"database"` + Options string `json:"options"` + VisibleDatabases string `json:"visible_databases"` } // SaveDbConnection 保存数据库连接配置 func (api *ConnectionAPI) SaveDbConnection(req SaveConnectionRequest) error { conn := &models.DbConnection{ - ID: req.ID, - Name: req.Name, - Type: req.Type, - Host: req.Host, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Database: req.Database, - Options: req.Options, + ID: req.ID, + Name: req.Name, + Type: req.Type, + Host: req.Host, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Database: req.Database, + Options: req.Options, + VisibleDatabases: req.VisibleDatabases, } return api.connService.SaveConnection(conn) } @@ -59,16 +61,17 @@ func (api *ConnectionAPI) ListDbConnections() ([]map[string]interface{}, error) timeFormat := "2006-01-02 15:04:05" for i, conn := range connections { result[i] = map[string]interface{}{ - "id": conn.ID, - "name": conn.Name, - "type": conn.Type, - "host": conn.Host, - "port": conn.Port, - "username": conn.Username, - "database": conn.Database, - "options": conn.Options, - "created_at": conn.CreatedAt.Format(timeFormat), - "updated_at": conn.UpdatedAt.Format(timeFormat), + "id": conn.ID, + "name": conn.Name, + "type": conn.Type, + "host": conn.Host, + "port": conn.Port, + "username": conn.Username, + "database": conn.Database, + "options": conn.Options, + "visible_databases": conn.VisibleDatabases, + "created_at": conn.CreatedAt.Format(timeFormat), + "updated_at": conn.UpdatedAt.Format(timeFormat), } } return result, nil @@ -79,7 +82,11 @@ func (api *ConnectionAPI) DeleteDbConnection(id uint) error { } func (api *ConnectionAPI) TestDbConnection(id uint) error { - return api.connService.TestConnection(id) + conn, err := api.connService.GetConnection(id) + if err != nil { + return err + } + return api.connService.TestConnection(conn) } // TestConnectionRequest 测试连接请求结构体(不保存数据) @@ -107,3 +114,29 @@ func (api *ConnectionAPI) TestDbConnectionWithParams(req TestConnectionRequest) req.ID, ) } + +// LoadAllDatabasesRequest 加载全部数据库请求结构体 +type LoadAllDatabasesRequest struct { + ID uint `json:"id"` + Type string `json:"type"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + Database string `json:"database"` + Options string `json:"options"` +} + +// LoadAllDatabases 加载全部数据库列表 +func (api *ConnectionAPI) LoadAllDatabases(req LoadAllDatabasesRequest) ([]string, error) { + return api.connService.LoadAllDatabases( + req.Type, + req.Host, + req.Port, + req.Username, + req.Password, + req.Database, + req.Options, + req.ID, + ) +} diff --git a/internal/storage/connection_service.go b/internal/storage/connection_service.go index 6f9452c..0068fad 100644 --- a/internal/storage/connection_service.go +++ b/internal/storage/connection_service.go @@ -1,6 +1,7 @@ package storage import ( + "context" "encoding/json" "fmt" "u-desk/internal/crypto" @@ -55,13 +56,14 @@ func (s *ConnectionService) SaveConnection(conn *models.DbConnection) error { if conn.ID > 0 { // 更新模式 updateData := map[string]interface{}{ - "name": conn.Name, - "type": conn.Type, - "host": conn.Host, - "port": conn.Port, - "username": conn.Username, - "database": conn.Database, - "options": conn.Options, + "name": conn.Name, + "type": conn.Type, + "host": conn.Host, + "port": conn.Port, + "username": conn.Username, + "database": conn.Database, + "options": conn.Options, + "visible_databases": conn.VisibleDatabases, } // 如果提供了新密码,加密后更新 @@ -111,7 +113,26 @@ func (s *ConnectionService) GetConnection(id uint) (*models.DbConnection, error) // DeleteConnection 删除连接配置 func (s *ConnectionService) DeleteConnection(id uint) error { - return s.db.Delete(&models.DbConnection{}, id).Error + var conn models.DbConnection + if err := s.db.First(&conn, id).Error; err != nil { + return nil // 连接不存在视为成功 + } + + // 使用事务删除 + return s.db.Transaction(func(tx *gorm.DB) error { + // 清理关联数据 + tx.Where("connection_id = ?", id).Delete(&models.SqlResultHistory{}) + tx.Where("connection_id = ?", id).Delete(&models.SqlTab{}) + + // 删除连接 + if err := tx.Delete(&conn).Error; err != nil { + return err + } + + // 关闭连接池 + dbclient.GetPool().CloseConnection(id, conn.Type) + return nil + }) } // TestConnection 测试连接(需要根据类型调用不同的测试方法) @@ -163,3 +184,127 @@ func testRedisConnection(host string, port int, password string) error { func testMongoConnection(host string, port int, username, password, database, authSource, authMechanism string) error { return dbclient.TestMongoConnectionWithOptions(host, port, username, password, database, authSource, authMechanism) } + +// TestConnectionWithParams 使用参数测试连接(不保存数据) +func (s *ConnectionService) TestConnectionWithParams(dbType, host string, port int, username, password, database, options string, id uint) error { + // 如果是编辑模式且有ID,获取已保存的密码 + if id > 0 && password == "" { + conn, err := s.GetConnection(id) + if err != nil { + return fmt.Errorf("获取连接信息失败: %v", err) + } + decryptPassword, err := crypto.DecryptPassword(conn.Password) + if err != nil { + return fmt.Errorf("密码解密失败: %v", err) + } + password = decryptPassword + } + + // 根据类型测试连接 + switch dbType { + case "mysql": + return testMySQLConnection(host, port, username, password, database) + case "redis": + return testRedisConnection(host, port, password) + case "mongo": + // 解析 Options 获取 MongoDB 连接参数 + authSource := "" + authMechanism := "" + if options != "" { + var opts map[string]interface{} + if err := json.Unmarshal([]byte(options), &opts); err == nil { + if as, ok := opts["authSource"].(string); ok && as != "" { + authSource = as + } + if am, ok := opts["authMechanism"].(string); ok && am != "" { + authMechanism = am + } + } + } + return testMongoConnection(host, port, username, password, database, authSource, authMechanism) + default: + return fmt.Errorf("不支持的数据库类型: %s", dbType) + } +} + +// LoadAllDatabases 加载全部数据库列表 +func (s *ConnectionService) LoadAllDatabases(dbType, host string, port int, username, password, database, options string, id uint) ([]string, error) { + // 如果是编辑模式且有ID,获取已保存的密码 + if id > 0 && password == "" { + conn, err := s.GetConnection(id) + if err != nil { + return nil, fmt.Errorf("获取连接信息失败: %v", err) + } + decryptPassword, err := crypto.DecryptPassword(conn.Password) + if err != nil { + return nil, fmt.Errorf("密码解密失败: %v", err) + } + password = decryptPassword + } + + // 根据类型加载数据库列表 + switch dbType { + case "mysql": + return loadMySQLDatabases(host, port, username, password, database) + case "mongo": + return loadMongoDatabases(host, port, username, password, database, options) + case "redis": + // Redis 没有数据库概念,返回空列表 + return []string{}, nil + default: + return nil, fmt.Errorf("不支持的数据库类型: %s", dbType) + } +} + +// loadMySQLDatabases 加载 MySQL 数据库列表 +func loadMySQLDatabases(host string, port int, username, password, defaultDatabase string) ([]string, error) { + config := &dbclient.MySQLConfig{ + Host: host, + Port: port, + Username: username, + Password: password, + Database: defaultDatabase, + } + client, err := dbclient.NewMySQLClient(config) + if err != nil { + return nil, err + } + defer client.Close() + + return client.ListDatabases(context.Background()) +} + +// loadMongoDatabases 加载 MongoDB 数据库列表 +func loadMongoDatabases(host string, port int, username, password, defaultDatabase, options string) ([]string, error) { + // 解析 Options 获取 MongoDB 连接参数 + authSource := "" + authMechanism := "" + if options != "" { + var opts map[string]interface{} + if err := json.Unmarshal([]byte(options), &opts); err == nil { + if as, ok := opts["authSource"].(string); ok && as != "" { + authSource = as + } + if am, ok := opts["authMechanism"].(string); ok && am != "" { + authMechanism = am + } + } + } + + mongoConfig := &dbclient.MongoConfig{ + Host: host, + Port: port, + Username: username, + Password: password, + Database: defaultDatabase, + AuthSource: authSource, + AuthMechanism: authMechanism, + } + client, err := dbclient.NewMongoClient(mongoConfig) + if err != nil { + return nil, err + } + defer client.Close() + + return client.ListDatabases(context.Background()) +} diff --git a/internal/storage/models/connection.go b/internal/storage/models/connection.go index b5c6a56..9cceac1 100644 --- a/internal/storage/models/connection.go +++ b/internal/storage/models/connection.go @@ -6,17 +6,18 @@ import ( // DbConnection 数据库连接配置 type DbConnection struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"type:varchar(100);not null" json:"name"` // 连接名称 - Type string `gorm:"type:varchar(20);not null" json:"type"` // 数据库类型: mysql/redis/mongo - Host string `gorm:"type:varchar(255);not null" json:"host"` // 主机地址 - Port int `gorm:"not null" json:"port"` // 端口 - Username string `gorm:"type:varchar(100)" json:"username"` // 用户名 - Password string `gorm:"type:varchar(500)" json:"-"` // 密码(加密存储,不返回) - Database string `gorm:"type:varchar(100)" json:"database"` // 数据库名(MySQL/MongoDB) - Options string `gorm:"type:text" json:"options"` // 额外选项(JSON格式) - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"type:varchar(100);not null" json:"name"` // 连接名称 + Type string `gorm:"type:varchar(20);not null" json:"type"` // 数据库类型: mysql/redis/mongo + Host string `gorm:"type:varchar(255);not null" json:"host"` // 主机地址 + Port int `gorm:"not null" json:"port"` // 端口 + Username string `gorm:"type:varchar(100)" json:"username"` // 用户名 + Password string `gorm:"type:varchar(500)" json:"-"` // 密码(加密存储,不返回) + Database string `gorm:"type:varchar(100)" json:"database"` // 数据库名(MySQL/MongoDB) + Options string `gorm:"type:text" json:"options"` // 额外选项(JSON格式) + VisibleDatabases string `gorm:"type:text" json:"visible_databases"` // 可见数据库列表(JSON数组,为空则全部可见) + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 指定表名 diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..bef4863 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,31 @@ +# 依赖 +node_modules/ + +# 构建产物 +dist/ +build/ + +# 自动生成的类型声明文件 +auto-imports.d.ts +components.d.ts + +# 缓存 +*.log +*.cache +.vite/ + +# 编辑器 +.vscode/* +!.vscode/extensions.json +.idea/ +*.swp +*.swo +*~ + +# 系统文件 +.DS_Store +Thumbs.db + +# 环境变量 +.env.local +.env.*.local diff --git a/web/src/App.vue b/web/src/App.vue index e22e4d2..00686e2 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -72,17 +72,16 @@