Private
Public Access
1
0

新增:连接管理、数据查询等功能

This commit is contained in:
2026-01-22 18:34:59 +08:00
parent 95d3a20292
commit 652f5e5d60
87 changed files with 15082 additions and 162 deletions

View File

@@ -0,0 +1,165 @@
package storage
import (
"encoding/json"
"fmt"
"go-desk/internal/crypto"
"go-desk/internal/dbclient"
"go-desk/internal/storage/models"
"gorm.io/gorm"
)
// ConnectionService 连接管理服务
type ConnectionService struct {
db *gorm.DB
}
// NewConnectionService 创建连接服务
func NewConnectionService() (*ConnectionService, error) {
db := GetDB()
if db == nil {
// 尝试重新初始化
var err error
db, err = Init()
if err != nil {
return nil, fmt.Errorf("数据库初始化失败: %v", err)
}
}
return &ConnectionService{db: db}, nil
}
// SaveConnection 保存连接配置
func (s *ConnectionService) SaveConnection(conn *models.DbConnection) error {
if conn.Name == "" {
return fmt.Errorf("连接名称不能为空")
}
if conn.Type == "" {
return fmt.Errorf("数据库类型不能为空")
}
if conn.Host == "" {
return fmt.Errorf("主机地址不能为空")
}
// 检查名称是否重复(排除当前记录)
var count int64
query := s.db.Model(&models.DbConnection{}).Where("name = ?", conn.Name)
if conn.ID > 0 {
query = query.Where("id != ?", conn.ID)
}
query.Count(&count)
if count > 0 {
return fmt.Errorf("连接名称已存在")
}
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,
}
// 如果提供了新密码,加密后更新
if conn.Password != "" {
encrypted, err := crypto.EncryptPassword(conn.Password)
if err != nil {
return fmt.Errorf("密码加密失败: %v", err)
}
updateData["password"] = encrypted
}
// 如果密码为空,不更新密码字段(保留原密码)
return s.db.Model(&models.DbConnection{}).Where("id = ?", conn.ID).Updates(updateData).Error
}
// 新增模式 - 必须提供密码
if conn.Password == "" {
return fmt.Errorf("新增连接时密码不能为空")
}
// 加密密码
encrypted, err := crypto.EncryptPassword(conn.Password)
if err != nil {
return fmt.Errorf("密码加密失败: %v", err)
}
conn.Password = encrypted
return s.db.Create(conn).Error
}
// ListConnections 获取连接列表
func (s *ConnectionService) ListConnections() ([]models.DbConnection, error) {
var connections []models.DbConnection
err := s.db.Order("created_at DESC").Find(&connections).Error
return connections, err
}
// GetConnection 获取连接详情
func (s *ConnectionService) GetConnection(id uint) (*models.DbConnection, error) {
var conn models.DbConnection
err := s.db.First(&conn, id).Error
if err != nil {
return nil, err
}
return &conn, nil
}
// DeleteConnection 删除连接配置
func (s *ConnectionService) DeleteConnection(id uint) error {
return s.db.Delete(&models.DbConnection{}, id).Error
}
// TestConnection 测试连接(需要根据类型调用不同的测试方法)
func (s *ConnectionService) TestConnection(conn *models.DbConnection) error {
// 解密密码用于测试
password, err := crypto.DecryptPassword(conn.Password)
if err != nil {
return fmt.Errorf("密码解密失败: %v", err)
}
// 根据类型测试连接
switch conn.Type {
case "mysql":
return testMySQLConnection(conn.Host, conn.Port, conn.Username, password, conn.Database)
case "redis":
return testRedisConnection(conn.Host, conn.Port, password)
case "mongo":
// 解析 Options 获取 MongoDB 连接参数
authSource := ""
authMechanism := ""
if conn.Options != "" {
var opts map[string]interface{}
if err := json.Unmarshal([]byte(conn.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(conn.Host, conn.Port, conn.Username, password, conn.Database, authSource, authMechanism)
default:
return fmt.Errorf("不支持的数据库类型: %s", conn.Type)
}
}
// testMySQLConnection 测试 MySQL 连接
func testMySQLConnection(host string, port int, username, password, database string) error {
return dbclient.TestMySQLConnection(host, port, username, password, database)
}
// testRedisConnection 测试 Redis 连接
func testRedisConnection(host string, port int, password string) error {
return dbclient.TestRedisConnection(host, port, password)
}
// testMongoConnection 测试 MongoDB 连接
func testMongoConnection(host string, port int, username, password, database, authSource, authMechanism string) error {
return dbclient.TestMongoConnectionWithOptions(host, port, username, password, database, authSource, authMechanism)
}

View File

@@ -0,0 +1,25 @@
package models
import (
"time"
)
// 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"`
}
// TableName 指定表名
func (DbConnection) TableName() string {
return "db_connection"
}

View File

@@ -0,0 +1,20 @@
package models
import (
"time"
)
// SqlFile SQL 文件记录
type SqlFile struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"type:varchar(200);not null" json:"name"` // 文件名
Path string `gorm:"type:varchar(500);not null;uniqueIndex" json:"path"` // 文件路径
Content string `gorm:"type:text" json:"content"` // 文件内容
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (SqlFile) TableName() string {
return "sql_file"
}

View File

@@ -0,0 +1,24 @@
package models
import (
"time"
)
// SqlResultHistory SQL 执行结果历史
type SqlResultHistory struct {
ID uint `gorm:"primaryKey" json:"id"`
ConnectionID uint `gorm:"index;not null" json:"connection_id"` // 连接ID
Database string `gorm:"type:varchar(100)" json:"database"` // 数据库名
Sql string `gorm:"type:text;not null" json:"sql"` // SQL语句
Type string `gorm:"type:varchar(20);not null" json:"type"` // 结果类型: query/update/command
Data string `gorm:"type:text" json:"data"` // 结果数据(JSON)
Columns string `gorm:"type:text" json:"columns"` // 列信息(JSON)
RowsAffected int `gorm:"default:0" json:"rows_affected"` // 影响行数
ExecutionTime int64 `gorm:"default:0" json:"execution_time"` // 执行时间(毫秒)
CreatedAt time.Time `json:"created_at"`
}
// TableName 指定表名
func (SqlResultHistory) TableName() string {
return "sql_result_history"
}

View File

@@ -0,0 +1,21 @@
package models
import (
"time"
)
// SqlTab SQL 编辑器标签页
type SqlTab struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"type:varchar(100);not null" json:"title"` // 标签页标题
Content string `gorm:"type:text" json:"content"` // SQL 内容
ConnectionID *uint `gorm:"index" json:"connection_id"` // 关联的连接ID可为空
Order int `gorm:"default:0" json:"order"` // 排序顺序
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (SqlTab) TableName() string {
return "sql_tab"
}

View File

@@ -0,0 +1,70 @@
package repository
import (
"go-desk/internal/storage"
"go-desk/internal/storage/models"
"gorm.io/gorm"
)
type ConnectionRepository interface {
Save(conn *models.DbConnection) error
FindAll() ([]models.DbConnection, error)
FindByID(id uint) (*models.DbConnection, error)
Delete(id uint) error
FindByName(name string, excludeID uint) (*models.DbConnection, error)
}
type connectionRepository struct {
db *gorm.DB
}
func NewConnectionRepository() (ConnectionRepository, error) {
db := storage.GetDB()
if db == nil {
var err error
db, err = storage.Init()
if err != nil {
return nil, err
}
}
return &connectionRepository{db}, nil
}
func (r *connectionRepository) Save(conn *models.DbConnection) error {
if conn.ID > 0 {
return r.db.Model(&models.DbConnection{}).Where("id = ?", conn.ID).Updates(conn).Error
}
return r.db.Create(conn).Error
}
func (r *connectionRepository) FindAll() ([]models.DbConnection, error) {
var connections []models.DbConnection
return connections, r.db.Order("created_at DESC").Find(&connections).Error
}
func (r *connectionRepository) FindByID(id uint) (*models.DbConnection, error) {
var conn models.DbConnection
err := r.db.First(&conn, id).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &conn, err
}
func (r *connectionRepository) Delete(id uint) error {
return r.db.Delete(&models.DbConnection{}, id).Error
}
func (r *connectionRepository) FindByName(name string, excludeID uint) (*models.DbConnection, error) {
var conn models.DbConnection
query := r.db.Where("name = ?", name)
if excludeID > 0 {
query = query.Where("id != ?", excludeID)
}
err := query.First(&conn).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &conn, err
}

View File

@@ -0,0 +1,110 @@
package repository
import (
"encoding/json"
"go-desk/internal/storage"
"go-desk/internal/storage/models"
"gorm.io/gorm"
"time"
)
type ResultRepository interface {
Save(connectionID uint, database, sql string, resultType string, data interface{}, columns []string, rowsAffected int, executionTime int64) (*models.SqlResultHistory, error)
FindByID(id uint) (*models.SqlResultHistory, error)
FindByConnection(connectionID uint, limit int) ([]models.SqlResultHistory, error)
Search(connectionID *uint, keyword string, limit, offset int) ([]models.SqlResultHistory, int64, error)
Delete(id uint) error
DeleteByConnection(connectionID uint) error
DeleteOld(keepDays int) error
}
type resultRepository struct {
db *gorm.DB
}
func NewResultRepository() (ResultRepository, error) {
db := storage.GetDB()
if db == nil {
var err error
db, err = storage.Init()
if err != nil {
return nil, err
}
}
return &resultRepository{db}, nil
}
func (r *resultRepository) Save(connectionID uint, database, sql string, resultType string, data interface{}, columns []string, rowsAffected int, executionTime int64) (*models.SqlResultHistory, error) {
dataJSON, _ := json.Marshal(data)
columnsJSON, _ := json.Marshal(columns)
history := &models.SqlResultHistory{
ConnectionID: connectionID,
Database: database,
Sql: sql,
Type: resultType,
Data: string(dataJSON),
Columns: string(columnsJSON),
RowsAffected: rowsAffected,
ExecutionTime: executionTime,
}
return history, r.db.Create(history).Error
}
func (r *resultRepository) FindByID(id uint) (*models.SqlResultHistory, error) {
var history models.SqlResultHistory
err := r.db.First(&history, id).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &history, err
}
func (r *resultRepository) FindByConnection(connectionID uint, limit int) ([]models.SqlResultHistory, error) {
var histories []models.SqlResultHistory
query := r.db.Where("connection_id = ?", connectionID).Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
return histories, query.Find(&histories).Error
}
func (r *resultRepository) Search(connectionID *uint, keyword string, limit, offset int) ([]models.SqlResultHistory, int64, error) {
query := r.db.Model(&models.SqlResultHistory{})
if connectionID != nil {
query = query.Where("connection_id = ?", *connectionID)
}
if keyword != "" {
query = query.Where("sql LIKE ? OR database LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var histories []models.SqlResultHistory
query = query.Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
return histories, total, query.Find(&histories).Error
}
func (r *resultRepository) Delete(id uint) error {
return r.db.Delete(&models.SqlResultHistory{}, id).Error
}
func (r *resultRepository) DeleteByConnection(connectionID uint) error {
return r.db.Where("connection_id = ?", connectionID).Delete(&models.SqlResultHistory{}).Error
}
func (r *resultRepository) DeleteOld(keepDays int) error {
return r.db.Where("created_at < ?", time.Now().AddDate(0, 0, -keepDays)).Delete(&models.SqlResultHistory{}).Error
}

View File

@@ -0,0 +1,55 @@
package repository
import (
"go-desk/internal/storage"
"go-desk/internal/storage/models"
"gorm.io/gorm"
)
type TabRepository interface {
SaveAll(tabs []models.SqlTab) error
FindAll() ([]models.SqlTab, error)
Delete(id uint) error
DeleteAll() error
}
type tabRepository struct {
db *gorm.DB
}
func NewTabRepository() (TabRepository, error) {
db := storage.GetDB()
if db == nil {
var err error
db, err = storage.Init()
if err != nil {
return nil, err
}
}
return &tabRepository{db}, nil
}
func (r *tabRepository) SaveAll(tabs []models.SqlTab) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("1=1").Delete(&models.SqlTab{}).Error; err != nil {
return err
}
if len(tabs) > 0 {
return tx.Create(&tabs).Error
}
return nil
})
}
func (r *tabRepository) FindAll() ([]models.SqlTab, error) {
var tabs []models.SqlTab
return tabs, r.db.Order("`order` ASC, created_at ASC").Find(&tabs).Error
}
func (r *tabRepository) Delete(id uint) error {
return r.db.Delete(&models.SqlTab{}, id).Error
}
func (r *tabRepository) DeleteAll() error {
return r.db.Where("1=1").Delete(&models.SqlTab{}).Error
}

View File

@@ -0,0 +1,57 @@
package storage
import (
"go-desk/internal/storage/models"
"os"
"path/filepath"
"time"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var globalDB *gorm.DB
func Init() (*gorm.DB, error) {
if globalDB != nil {
return globalDB, nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
dataDir := filepath.Join(homeDir, ".go-desk")
os.MkdirAll(dataDir, 0755)
dbPath := filepath.Join(dataDir, "db-cli.db")
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, err
}
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(time.Hour)
if err := db.AutoMigrate(
&models.DbConnection{},
&models.SqlTab{},
&models.SqlResultHistory{},
); err != nil {
return nil, err
}
globalDB = db
return globalDB, nil
}
func GetDB() *gorm.DB {
return globalDB
}