From f7b685cfad11c760133def6115b309b5d587e513 Mon Sep 17 00:00:00 2001 From: kanzihuang Date: Sun, 27 Aug 2023 11:07:29 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=AE=9E=E7=8E=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=AE=9E=E4=BE=8B=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/ops/db/InstanceEdit.vue | 213 ++++++++++++++++++ .../src/views/ops/db/InstanceList.vue | 182 +++++++++++++++ mayfly_go_web/src/views/ops/db/api.ts | 6 + server/internal/db/api/form/instance.go | 14 ++ server/internal/db/api/instance.go | 71 ++++++ server/internal/db/api/vo/instance.go | 24 ++ server/internal/db/application/application.go | 5 + server/internal/db/application/instance.go | 92 ++++++++ server/internal/db/domain/entity/db.go | 5 - server/internal/db/domain/entity/instance.go | 50 ++++ server/internal/db/domain/entity/query.go | 15 ++ .../internal/db/domain/repository/instance.go | 25 ++ .../db/infrastructure/persistence/instance.go | 57 +++++ .../infrastructure/persistence/persistence.go | 5 + server/internal/db/router/instance.go | 32 +++ server/internal/db/router/router.go | 1 + server/migrations/2022.go | 3 + 17 files changed, 795 insertions(+), 5 deletions(-) create mode 100644 mayfly_go_web/src/views/ops/db/InstanceEdit.vue create mode 100644 mayfly_go_web/src/views/ops/db/InstanceList.vue create mode 100644 server/internal/db/api/form/instance.go create mode 100644 server/internal/db/api/instance.go create mode 100644 server/internal/db/api/vo/instance.go create mode 100644 server/internal/db/application/instance.go create mode 100644 server/internal/db/domain/entity/instance.go create mode 100644 server/internal/db/domain/repository/instance.go create mode 100644 server/internal/db/infrastructure/persistence/instance.go create mode 100644 server/internal/db/router/instance.go diff --git a/mayfly_go_web/src/views/ops/db/InstanceEdit.vue b/mayfly_go_web/src/views/ops/db/InstanceEdit.vue new file mode 100644 index 00000000..123af6ef --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/InstanceEdit.vue @@ -0,0 +1,213 @@ + + + + diff --git a/mayfly_go_web/src/views/ops/db/InstanceList.vue b/mayfly_go_web/src/views/ops/db/InstanceList.vue new file mode 100644 index 00000000..3e498cba --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/InstanceList.vue @@ -0,0 +1,182 @@ + + + + diff --git a/mayfly_go_web/src/views/ops/db/api.ts b/mayfly_go_web/src/views/ops/db/api.ts index 4d03e9db..6e03b3b6 100644 --- a/mayfly_go_web/src/views/ops/db/api.ts +++ b/mayfly_go_web/src/views/ops/db/api.ts @@ -26,4 +26,10 @@ export const dbApi = { deleteDbSql: Api.newDelete('/dbs/{id}/sql'), // 获取数据库sql执行记录 getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'), + + // 获取权限列表 + instances: Api.newGet('/instances'), + saveInstance: Api.newPost('/instances'), + getInstancePwd: Api.newGet('/instances/{id}/pwd'), + deleteInstance: Api.newDelete('/instances/{id}'), }; diff --git a/server/internal/db/api/form/instance.go b/server/internal/db/api/form/instance.go new file mode 100644 index 00000000..65fe1968 --- /dev/null +++ b/server/internal/db/api/form/instance.go @@ -0,0 +1,14 @@ +package form + +type InstanceForm struct { + Id uint64 `json:"id"` + Name string `binding:"required" json:"name"` + Type string `binding:"required" json:"type"` // 类型,mysql oracle等 + Host string `binding:"required" json:"host"` + Port int `binding:"required" json:"port"` + Username string `binding:"required" json:"username"` + Password string `json:"password"` + Params string `json:"params"` + Remark string `json:"remark"` + SshTunnelMachineId int `json:"sshTunnelMachineId"` +} diff --git a/server/internal/db/api/instance.go b/server/internal/db/api/instance.go new file mode 100644 index 00000000..e53ffe16 --- /dev/null +++ b/server/internal/db/api/instance.go @@ -0,0 +1,71 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "mayfly-go/internal/db/api/form" + "mayfly-go/internal/db/api/vo" + "mayfly-go/internal/db/application" + "mayfly-go/internal/db/domain/entity" + msgapp "mayfly-go/internal/msg/application" + "mayfly-go/pkg/biz" + "mayfly-go/pkg/ginx" + "mayfly-go/pkg/req" + "mayfly-go/pkg/utils/cryptox" + "strconv" + "strings" +) + +type Instance struct { + InstanceApp application.Instance + MsgApp msgapp.Msg +} + +// @router /api/instances [get] +func (d *Instance) Instances(rc *req.Ctx) { + queryCond, page := ginx.BindQueryAndPage[*entity.InstanceQuery](rc.GinCtx, new(entity.InstanceQuery)) + rc.ResData = d.InstanceApp.GetPageList(queryCond, page, new([]vo.SelectDataInstanceVO)) +} + +func (d *Instance) SaveInstance(rc *req.Ctx) { + form := &form.InstanceForm{} + instance := ginx.BindJsonAndCopyTo[*entity.Instance](rc.GinCtx, form, new(entity.Instance)) + + // 密码解密,并使用解密后的赋值 + originPwd, err := cryptox.DefaultRsaDecrypt(form.Password, true) + biz.ErrIsNilAppendErr(err, "解密密码错误: %s") + instance.Password = originPwd + + // 密码脱敏记录日志 + form.Password = "****" + rc.ReqParam = form + + instance.SetBaseInfo(rc.LoginAccount) + d.InstanceApp.Save(instance) +} + +// 获取数据库实例密码,由于数据库是加密存储,故提供该接口展示原文密码 +func (d *Instance) GetInstancePwd(rc *req.Ctx) { + dbId := GetInstanceId(rc.GinCtx) + dbEntity := d.InstanceApp.GetById(dbId, "Password") + dbEntity.PwdDecrypt() + rc.ResData = dbEntity.Password +} + +func (d *Instance) DeleteInstance(rc *req.Ctx) { + idsStr := ginx.PathParam(rc.GinCtx, "dbId") + rc.ReqParam = idsStr + ids := strings.Split(idsStr, ",") + + for _, v := range ids { + value, err := strconv.Atoi(v) + biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s") + dbId := uint64(value) + d.InstanceApp.Delete(dbId) + } +} + +func GetInstanceId(g *gin.Context) uint64 { + dbId, _ := strconv.Atoi(g.Param("dbId")) + biz.IsTrue(dbId > 0, "dbId错误") + return uint64(dbId) +} diff --git a/server/internal/db/api/vo/instance.go b/server/internal/db/api/vo/instance.go new file mode 100644 index 00000000..315259b6 --- /dev/null +++ b/server/internal/db/api/vo/instance.go @@ -0,0 +1,24 @@ +package vo + +import "time" + +type SelectDataInstanceVO struct { + //models.BaseModel + Id *int64 `json:"id"` + Name *string `json:"name"` + Host *string `json:"host"` + Port *int `json:"port"` + Type *string `json:"type"` + Params *string `json:"params"` + Username *string `json:"username"` + Remark *string `json:"remark"` + CreateTime *time.Time `json:"createTime"` + Creator *string `json:"creator"` + CreatorId *int64 `json:"creatorId"` + + UpdateTime *time.Time `json:"updateTime"` + Modifier *string `json:"modifier"` + ModifierId *int64 `json:"modifierId"` + + SshTunnelMachineId int `json:"sshTunnelMachineId"` +} diff --git a/server/internal/db/application/application.go b/server/internal/db/application/application.go index c66b2841..3dd32c4e 100644 --- a/server/internal/db/application/application.go +++ b/server/internal/db/application/application.go @@ -5,10 +5,15 @@ import ( ) var ( + instanceApp Instance = newInstanceApp(persistence.GetInstanceRepo()) dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo()) dbSqlExecApp DbSqlExec = newDbSqlExecApp(persistence.GetDbSqlExecRepo()) ) +func GetInstanceApp() Instance { + return instanceApp +} + func GetDbApp() Db { return dbApp } diff --git a/server/internal/db/application/instance.go b/server/internal/db/application/instance.go new file mode 100644 index 00000000..d29eacce --- /dev/null +++ b/server/internal/db/application/instance.go @@ -0,0 +1,92 @@ +package application + +import ( + "mayfly-go/internal/db/domain/entity" + "mayfly-go/internal/db/domain/repository" + "mayfly-go/pkg/biz" + "mayfly-go/pkg/model" +) + +type Instance interface { + // 分页获取 + GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] + + Count(condition *entity.InstanceQuery) int64 + + // 根据条件获取 + GetInstanceBy(condition *entity.Instance, cols ...string) error + + // 根据id获取 + GetById(id uint64, cols ...string) *entity.Instance + + Save(instanceEntity *entity.Instance) + + // 删除数据库信息 + Delete(id uint64) +} + +func newInstanceApp(InstanceRepo repository.Instance) Instance { + return &instanceAppImpl{ + instanceRepo: InstanceRepo, + } +} + +type instanceAppImpl struct { + instanceRepo repository.Instance +} + +// 分页获取数据库信息列表 +func (d *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] { + return d.instanceRepo.GetInstanceList(condition, pageParam, toEntity, orderBy...) +} + +func (d *instanceAppImpl) Count(condition *entity.InstanceQuery) int64 { + return d.instanceRepo.Count(condition) +} + +// 根据条件获取 +func (d *instanceAppImpl) GetInstanceBy(condition *entity.Instance, cols ...string) error { + return d.instanceRepo.GetInstance(condition, cols...) +} + +// 根据id获取 +func (d *instanceAppImpl) GetById(id uint64, cols ...string) *entity.Instance { + return d.instanceRepo.GetById(id, cols...) +} + +func (d *instanceAppImpl) Save(instanceEntity *entity.Instance) { + // 默认tcp连接 + instanceEntity.Network = instanceEntity.GetNetwork() + + // 测试连接 + // todo 测试数据库连接 + //if instanceEntity.Password != "" { + // TestConnection(instanceEntity) + //} + + // 查找是否存在该库 + oldInstance := &entity.Instance{Host: instanceEntity.Host, Port: instanceEntity.Port, Username: instanceEntity.Username} + if instanceEntity.SshTunnelMachineId > 0 { + oldInstance.SshTunnelMachineId = instanceEntity.SshTunnelMachineId + } + + err := d.GetInstanceBy(oldInstance) + if instanceEntity.Id == 0 { + biz.NotEmpty(instanceEntity.Password, "密码不能为空") + biz.IsTrue(err != nil, "该数据库实例已存在") + instanceEntity.PwdEncrypt() + d.instanceRepo.Insert(instanceEntity) + } else { + // 如果存在该库,则校验修改的库是否为该库 + if err == nil { + biz.IsTrue(oldInstance.Id == instanceEntity.Id, "该数据库实例已存在") + } + instanceEntity.PwdEncrypt() + d.instanceRepo.Update(instanceEntity) + } +} + +func (d *instanceAppImpl) Delete(id uint64) { + // todo 删除数据库库实例前必须删除关联数据库 + d.instanceRepo.Delete(id) +} diff --git a/server/internal/db/domain/entity/db.go b/server/internal/db/domain/entity/db.go index 1d18fc14..947d327d 100644 --- a/server/internal/db/domain/entity/db.go +++ b/server/internal/db/domain/entity/db.go @@ -46,8 +46,3 @@ func (d *Db) PwdDecrypt() { // 密码替换为解密后的密码 d.Password = utils.PwdAesDecrypt(d.Password) } - -const ( - DbTypeMysql = "mysql" - DbTypePostgres = "postgres" -) diff --git a/server/internal/db/domain/entity/instance.go b/server/internal/db/domain/entity/instance.go new file mode 100644 index 00000000..a3b6372b --- /dev/null +++ b/server/internal/db/domain/entity/instance.go @@ -0,0 +1,50 @@ +package entity + +import ( + "fmt" + "mayfly-go/internal/common/utils" + "mayfly-go/pkg/model" +) + +type Instance struct { + model.Model + + Name string `orm:"column(name)" json:"name"` + Type string `orm:"column(type)" json:"type"` // 类型,mysql oracle等 + Host string `orm:"column(host)" json:"host"` + Port int `orm:"column(port)" json:"port"` + Network string `orm:"column(network)" json:"network"` + Username string `orm:"column(username)" json:"username"` + Password string `orm:"column(password)" json:"-"` + Params string `orm:"column(params)" json:"params"` + Remark string `orm:"column(remark)" json:"remark"` + SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id +} + +// 获取数据库连接网络, 若没有使用ssh隧道,则直接返回。否则返回拼接的网络需要注册至指定dial +func (d *Instance) GetNetwork() string { + network := d.Network + if d.SshTunnelMachineId <= 0 { + if network == "" { + return "tcp" + } else { + return network + } + } + return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId) +} + +func (d *Instance) PwdEncrypt() { + // 密码替换为加密后的密码 + d.Password = utils.PwdAesEncrypt(d.Password) +} + +func (d *Instance) PwdDecrypt() { + // 密码替换为解密后的密码 + d.Password = utils.PwdAesDecrypt(d.Password) +} + +const ( + DbTypeMysql = "mysql" + DbTypePostgres = "postgres" +) diff --git a/server/internal/db/domain/entity/query.go b/server/internal/db/domain/entity/query.go index 821e4f2e..3111b1f5 100644 --- a/server/internal/db/domain/entity/query.go +++ b/server/internal/db/domain/entity/query.go @@ -2,6 +2,21 @@ package entity import "mayfly-go/pkg/model" +// 数据库实例查询 +type InstanceQuery struct { + model.Model + + Name string `orm:"column(name)" json:"name" form:"name"` + Type string `orm:"column(type)" json:"type"` // 类型,mysql oracle等 + Host string `orm:"column(host)" json:"host"` + Port int `orm:"column(port)" json:"port"` + Network string `orm:"column(network)" json:"network"` + Username string `orm:"column(username)" json:"username"` + Password string `orm:"column(password)" json:"-"` + Params string `orm:"column(params)" json:"params"` + Remark string `orm:"column(remark)" json:"remark"` +} + // 数据库查询实体,不与数据库表字段一一对应 type DbQuery struct { model.Model diff --git a/server/internal/db/domain/repository/instance.go b/server/internal/db/domain/repository/instance.go new file mode 100644 index 00000000..d5950a53 --- /dev/null +++ b/server/internal/db/domain/repository/instance.go @@ -0,0 +1,25 @@ +package repository + +import ( + "mayfly-go/internal/db/domain/entity" + "mayfly-go/pkg/model" +) + +type Instance interface { + // 分页获取数据库实例信息列表 + GetInstanceList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] + + Count(condition *entity.InstanceQuery) int64 + + // 根据条件获取账号信息 + GetInstance(condition *entity.Instance, cols ...string) error + + // 根据id获取 + GetById(id uint64, cols ...string) *entity.Instance + + Insert(db *entity.Instance) + + Update(db *entity.Instance) + + Delete(id uint64) +} diff --git a/server/internal/db/infrastructure/persistence/instance.go b/server/internal/db/infrastructure/persistence/instance.go new file mode 100644 index 00000000..e89ad0bd --- /dev/null +++ b/server/internal/db/infrastructure/persistence/instance.go @@ -0,0 +1,57 @@ +package persistence + +import ( + "mayfly-go/internal/db/domain/entity" + "mayfly-go/internal/db/domain/repository" + "mayfly-go/pkg/biz" + "mayfly-go/pkg/gormx" + "mayfly-go/pkg/model" +) + +type instanceRepoImpl struct{} + +func newInstanceRepo() repository.Instance { + return new(instanceRepoImpl) +} + +// 分页获取数据库信息列表 +func (d *instanceRepoImpl) GetInstanceList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] { + qd := gormx.NewQuery(new(entity.Instance)). + Eq("host", condition.Host). + Eq("port", condition.Port). + Eq("username", condition.Username). + Like("name", condition.Name) + return gormx.PageQuery(qd, pageParam, toEntity) +} + +func (d *instanceRepoImpl) Count(condition *entity.InstanceQuery) int64 { + where := make(map[string]any) + return gormx.CountByCond(new(entity.Instance), where) +} + +// 根据条件获数据库实例信息 +func (d *instanceRepoImpl) GetInstance(condition *entity.Instance, cols ...string) error { + return gormx.GetBy(condition, cols...) +} + +// 根据id获取数据库实例 +func (d *instanceRepoImpl) GetById(id uint64, cols ...string) *entity.Instance { + instance := new(entity.Instance) + if err := gormx.GetById(instance, id, cols...); err != nil { + return nil + + } + return instance +} + +func (d *instanceRepoImpl) Insert(db *entity.Instance) { + biz.ErrIsNil(gormx.Insert(db), "新增数据库实例失败") +} + +func (d *instanceRepoImpl) Update(db *entity.Instance) { + biz.ErrIsNil(gormx.UpdateById(db), "更新数据库实例失败") +} + +func (d *instanceRepoImpl) Delete(id uint64) { + gormx.DeleteById(new(entity.Instance), id) +} diff --git a/server/internal/db/infrastructure/persistence/persistence.go b/server/internal/db/infrastructure/persistence/persistence.go index 51964b6c..01998516 100644 --- a/server/internal/db/infrastructure/persistence/persistence.go +++ b/server/internal/db/infrastructure/persistence/persistence.go @@ -3,11 +3,16 @@ package persistence import "mayfly-go/internal/db/domain/repository" var ( + instanceRepo repository.Instance = newInstanceRepo() dbRepo repository.Db = newDbRepo() dbSqlRepo repository.DbSql = newDbSqlRepo() dbSqlExecRepo repository.DbSqlExec = newDbSqlExecRepo() ) +func GetInstanceRepo() repository.Instance { + return instanceRepo +} + func GetDbRepo() repository.Db { return dbRepo } diff --git a/server/internal/db/router/instance.go b/server/internal/db/router/instance.go new file mode 100644 index 00000000..160a2cbc --- /dev/null +++ b/server/internal/db/router/instance.go @@ -0,0 +1,32 @@ +package router + +import ( + "mayfly-go/internal/db/api" + "mayfly-go/internal/db/application" + msgapp "mayfly-go/internal/msg/application" + "mayfly-go/pkg/req" + + "github.com/gin-gonic/gin" +) + +func InitInstanceRouter(router *gin.RouterGroup) { + instances := router.Group("/instances") + + d := &api.Instance{ + InstanceApp: application.GetInstanceApp(), + MsgApp: msgapp.GetMsgApp(), + } + + reqs := [...]*req.Conf{ + // 获取数据库列表 + req.NewGet("", d.Instances), + + req.NewPost("", d.SaveInstance).Log(req.NewLogSave("db-保存数据库实例信息")), + + req.NewGet(":dbId/pwd", d.GetInstancePwd), + + req.NewDelete(":dbId", d.DeleteInstance).Log(req.NewLogSave("db-删除数据库实例")), + } + + req.BatchSetGroup(instances, reqs[:]) +} diff --git a/server/internal/db/router/router.go b/server/internal/db/router/router.go index f3afd0f5..8808b91f 100644 --- a/server/internal/db/router/router.go +++ b/server/internal/db/router/router.go @@ -3,6 +3,7 @@ package router import "github.com/gin-gonic/gin" func Init(router *gin.RouterGroup) { + InitInstanceRouter(router) InitDbRouter(router) InitDbSqlExecRouter(router) } diff --git a/server/migrations/2022.go b/server/migrations/2022.go index c5b2c319..94698acf 100644 --- a/server/migrations/2022.go +++ b/server/migrations/2022.go @@ -43,6 +43,9 @@ func T2022() *gormigrate.Migration { return err } + if err := tx.AutoMigrate(&entity2.Instance{}); err != nil { + return err + } if err := tx.AutoMigrate(&entity2.Db{}); err != nil { return err }