feat: 完善数据库信息保存以及项目、redis相关操作

This commit is contained in:
meilin.huang
2021-07-28 18:03:19 +08:00
parent 3ebc3ee14d
commit bda3920c1e
153 changed files with 5527 additions and 1017 deletions

3
.vscode/launch.json vendored
View File

@@ -4,6 +4,7 @@
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "mayfly-go",
"type": "go",
@@ -12,6 +13,6 @@
"program": "${fileDirname}/main.go",
"env": {},
"args": []
}
},
]
}

View File

@@ -6,18 +6,11 @@ type BizError struct {
err string
}
const (
SuccessCode = 200
SuccessMsg = "success"
BizErrorCode = 400
BizErrorMsg = "error"
ServerErrorCode = 500
ServerErrorMsg = "server error"
TokenErrorCode = 501
TokenErrorMsg = "token error"
var (
Success *BizError = NewBizErrCode(200, "success")
BizErr *BizError = NewBizErrCode(400, "biz error")
ServerError *BizError = NewBizErrCode(500, "server error")
PermissionErr *BizError = NewBizErrCode(501, "token error")
)
// 错误消息
@@ -32,7 +25,7 @@ func (e *BizError) Code() int16 {
// 创建业务逻辑错误结构体,默认为业务逻辑错误
func NewBizErr(msg string) *BizError {
return &BizError{code: BizErrorCode, err: msg}
return &BizError{code: BizErr.code, err: msg}
}
// 创建业务逻辑错误结构体可设置指定错误code

28
base/captcha/captcha.go Normal file
View File

@@ -0,0 +1,28 @@
package captcha
import (
"mayfly-go/base/biz"
"github.com/mojocn/base64Captcha"
)
var store = base64Captcha.DefaultMemStore
var driver base64Captcha.Driver = base64Captcha.DefaultDriverDigit
// 生成验证码
func Generate() (string, string) {
c := base64Captcha.NewCaptcha(driver, store)
// 获取
id, b64s, err := c.Generate()
biz.ErrIsNilAppendErr(err, "获取验证码错误: %s")
return id, b64s
}
// 验证验证码
func Verify(id string, val string) bool {
if id == "" || val == "" {
return false
}
// 同时清理掉这个图片
return store.Verify(id, val, true)
}

View File

@@ -4,9 +4,13 @@ import (
"flag"
"fmt"
"mayfly-go/base/utils"
"mayfly-go/base/utils/assert"
"path/filepath"
)
// 配置文件映射对象
var Conf *Config
func init() {
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
flag.Parse()
@@ -18,6 +22,8 @@ func init() {
if err := utils.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
panic(fmt.Sprintf("读取配置文件[%s]失败: %s", startConfigParam.ConfigFilePath, err.Error()))
}
// 校验配置文件内容信息
yc.Valid()
Conf = yc
}
@@ -33,12 +39,17 @@ var startConfigParam *CmdConfigParam
type Config struct {
App *App `yaml:"app"`
Server *Server `yaml:"server"`
Jwt *Jwt `yaml:"jwt"`
Redis *Redis `yaml:"redis"`
Mysql *Mysql `yaml:"mysql"`
Log *Log `yaml:"log"`
}
// 配置文件映射对象
var Conf *Config
// 配置文件内容校验
func (c *Config) Valid() {
assert.IsTrue(c.Jwt != nil, "配置文件的[jwt]信息不能为空")
c.Jwt.Valid()
}
// 获取执行可执行文件时,指定的启动参数
func getStartConfig() *CmdConfigParam {

13
base/config/jwt.go Normal file
View File

@@ -0,0 +1,13 @@
package config
import "mayfly-go/base/utils/assert"
type Jwt struct {
Key string `yaml:"key"`
ExpireTime uint64 `yaml:"expire-time"` // 过期时间,单位分钟
}
func (j *Jwt) Valid() {
assert.IsTrue(j.Key != "", "config.yml之 [jwt.key] 不能为空")
assert.IsTrue(j.ExpireTime != 0, "config.yml之 [jwt.expire-time] 不能为空")
}

30
base/config/log.go Normal file
View File

@@ -0,0 +1,30 @@
package config
import "path"
type Log struct {
Level string `yaml:"level"`
File *LogFile `yaml:"file"`
}
type LogFile struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
}
// 获取完整路径文件名
func (l *LogFile) GetFilename() string {
var filepath, filename string
if fp := l.Path; fp == "" {
filepath = "./"
} else {
filepath = fp
}
if fn := l.Name; fn == "" {
filename = "default.log"
} else {
filename = fn
}
return path.Join(filepath, filename)
}

View File

@@ -6,6 +6,7 @@ type Server struct {
Port int `yaml:"port"`
Model string `yaml:"model"`
Cors bool `yaml:"cors"`
Tls *Tls `yaml:"tls"`
Static *[]*Static `yaml:"static"`
StaticFile *[]*StaticFile `yaml:"static-file"`
}
@@ -23,3 +24,9 @@ type StaticFile struct {
RelativePath string `yaml:"relative-path"`
Filepath string `yaml:"filepath"`
}
type Tls struct {
Enable bool `yaml:"enable"` // 是否启用tls
KeyFile string `yaml:"key-file"` // 私钥文件路径
CertFile string `yaml:"cert-file"` // 证书文件路径
}

View File

@@ -9,14 +9,9 @@ import (
"reflect"
"runtime/debug"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
)
func init() {
log.SetFormatter(new(logger.LogFormatter))
log.SetReportCaller(true)
}
type LogInfo struct {
LogResp bool // 是否记录返回结果
Description string // 请求描述
@@ -37,7 +32,7 @@ func LogHandler(rc *ReqCtx) error {
return nil
}
lfs := log.Fields{}
lfs := logrus.Fields{}
if la := rc.LoginAccount; la != nil {
lfs["uid"] = la.Id
lfs["uname"] = la.Username
@@ -47,10 +42,10 @@ func LogHandler(rc *ReqCtx) error {
lfs[req.Method] = req.URL.Path
if err := rc.Err; err != nil {
log.WithFields(lfs).Error(getErrMsg(rc, err))
logger.Log.WithFields(lfs).Error(getErrMsg(rc, err))
return nil
}
log.WithFields(lfs).Info(getLogMsg(rc))
logger.Log.WithFields(lfs).Info(getLogMsg(rc))
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"mayfly-go/base/biz"
"mayfly-go/base/cache"
"mayfly-go/base/config"
"time"
)
@@ -37,7 +38,7 @@ type DefaultPermissionCodeRegistry struct {
func (r *DefaultPermissionCodeRegistry) SaveCodes(userId uint64, codes []string) {
if r.cache == nil {
r.cache = cache.NewTimedCache(30*time.Minute, 5*time.Second).WithUpdateAccessTime(true)
r.cache = cache.NewTimedCache(time.Minute*time.Duration(config.Conf.Jwt.ExpireTime), 5*time.Second)
}
r.cache.Put(fmt.Sprintf("%v", userId), codes)
}
@@ -79,7 +80,7 @@ func SetPermissionCodeRegistery(pcr PermissionCodeRegistry) {
var (
permissionCodeRegistry PermissionCodeRegistry = &DefaultPermissionCodeRegistry{}
permissionError = biz.NewBizErrCode(biz.TokenErrorCode, biz.TokenErrorMsg)
// permissionError = biz.NewBizErrCode(biz.TokenErrorCode, biz.TokenErrorMsg)
)
func PermissionHandler(rc *ReqCtx) error {
@@ -94,16 +95,16 @@ func PermissionHandler(rc *ReqCtx) error {
tokenStr = rc.GinCtx.Query("token")
}
if tokenStr == "" {
return permissionError
return biz.PermissionErr
}
loginAccount, err := ParseToken(tokenStr)
if err != nil || loginAccount == nil {
return permissionError
return biz.PermissionErr
}
// 权限不为nil并且permission code不为空则校验是否有权限code
if permission != nil && permission.Code != "" {
if !permissionCodeRegistry.HasCode(loginAccount.Id, permission.Code) {
return permissionError
return biz.PermissionErr
}
}

View File

@@ -3,6 +3,7 @@ package ctx
import (
"mayfly-go/base/ginx"
"mayfly-go/base/model"
"mayfly-go/base/utils/assert"
"time"
"github.com/gin-gonic/gin"
@@ -37,9 +38,7 @@ func (rc *ReqCtx) Handle(handler HandlerFunc) {
// 应用所有请求后置处理器
ApplyHandlerInterceptor(afterHandlers, rc)
}()
if ginCtx == nil {
panic("ginContext == nil")
}
assert.IsTrue(ginCtx != nil, "ginContext == nil")
// 默认为不记录请求参数可在handler回调函数中覆盖赋值
rc.ReqParam = 0

View File

@@ -4,15 +4,16 @@ import (
"errors"
"mayfly-go/base/biz"
"mayfly-go/base/config"
"mayfly-go/base/model"
"time"
"github.com/dgrijalva/jwt-go"
)
const (
JwtKey = "mykey"
ExpTime = time.Hour * 24 * 7
var (
JwtKey = config.Conf.Jwt.Key
ExpTime = config.Conf.Jwt.ExpireTime
)
// 创建用户token
@@ -22,7 +23,7 @@ func CreateToken(userId uint64, username string) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": userId,
"username": username,
"exp": time.Now().Add(ExpTime).Unix(),
"exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(),
})
// 使用自定义字符串加密 and get the complete encoded token as a string

View File

@@ -1,7 +1,6 @@
package ginx
import (
"fmt"
"mayfly-go/base/biz"
"mayfly-go/base/global"
"mayfly-go/base/model"
@@ -13,14 +12,14 @@ import (
// 绑定并校验请求结构体参数
func BindJsonAndValid(g *gin.Context, data interface{}) {
if err := g.BindJSON(data); err != nil {
if err := g.ShouldBindJSON(data); err != nil {
panic(biz.NewBizErr(err.Error()))
}
}
// 绑定查询字符串到
func BindQuery(g *gin.Context, data interface{}) {
if err := g.BindQuery(data); err != nil {
if err := g.ShouldBindQuery(data); err != nil {
panic(biz.NewBizErr(err.Error()))
}
}
@@ -44,7 +43,6 @@ func QueryInt(g *gin.Context, qm string, defaultInt int) int {
// 获取路径参数
func PathParamInt(g *gin.Context, pm string) int {
value, _ := strconv.Atoi(g.Param(pm))
biz.IsTrue(value != 0, fmt.Sprintf("%s不存在", pm))
return value
}
@@ -64,7 +62,7 @@ func SuccessRes(g *gin.Context, data interface{}) {
func ErrorRes(g *gin.Context, err interface{}) {
switch t := err.(type) {
case *biz.BizError:
g.JSON(http.StatusOK, model.Error(t.Code(), t.Error()))
g.JSON(http.StatusOK, model.Error(t))
break
case error:
g.JSON(http.StatusOK, model.ServerError())

View File

@@ -1,17 +1,11 @@
package global
import (
"mayfly-go/base/config"
"mayfly-go/base/logger"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// 日志
var Log = logger.Log
// config.yml配置文件映射对象
var Config = config.Conf
// gorm
var Db *gorm.DB
var (
Log *logrus.Logger // 日志
Db *gorm.DB // gorm
)

View File

@@ -7,10 +7,14 @@ import (
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"time"
)
var client = &http.Client{}
// 默认超时
const DefTimeout = 60
@@ -22,6 +26,13 @@ type RequestWrapper struct {
header map[string]string
}
type MultipartFile struct {
FieldName string // 字段名
FileName string // 文件名
FilePath string // 文件路径,文件路径不为空,则优先读取文件路径的内容
Bytes []byte // 文件内容
}
// 创建一个请求
func NewRequest(url string) *RequestWrapper {
return &RequestWrapper{url: url}
@@ -31,12 +42,21 @@ func (r *RequestWrapper) Url(url string) *RequestWrapper {
r.url = url
return r
}
func (r *RequestWrapper) Header(name, value string) *RequestWrapper {
if r.header == nil {
r.header = make(map[string]string)
}
r.header[name] = value
return r
}
func (r *RequestWrapper) Timeout(timeout int) *RequestWrapper {
r.timeout = timeout
return r
}
func (r *RequestWrapper) GetByParam(paramMap map[string]string) ResponseWrapper {
func (r *RequestWrapper) GetByParam(paramMap map[string]string) *ResponseWrapper {
var params string
for k, v := range paramMap {
if params != "" {
@@ -50,13 +70,13 @@ func (r *RequestWrapper) GetByParam(paramMap map[string]string) ResponseWrapper
return r.Get()
}
func (r *RequestWrapper) Get() ResponseWrapper {
func (r *RequestWrapper) Get() *ResponseWrapper {
r.method = "GET"
r.body = nil
return request(r)
}
func (r *RequestWrapper) PostJson(body string) ResponseWrapper {
func (r *RequestWrapper) PostJson(body string) *ResponseWrapper {
buf := bytes.NewBufferString(body)
r.method = "POST"
r.body = buf
@@ -67,7 +87,7 @@ func (r *RequestWrapper) PostJson(body string) ResponseWrapper {
return request(r)
}
func (r *RequestWrapper) PostObj(body interface{}) ResponseWrapper {
func (r *RequestWrapper) PostObj(body interface{}) *ResponseWrapper {
marshal, err := json.Marshal(body)
if err != nil {
return createRequestError(errors.New("解析json obj错误"))
@@ -75,7 +95,7 @@ func (r *RequestWrapper) PostObj(body interface{}) ResponseWrapper {
return r.PostJson(string(marshal))
}
func (r *RequestWrapper) PostParams(params string) ResponseWrapper {
func (r *RequestWrapper) PostParams(params string) *ResponseWrapper {
buf := bytes.NewBufferString(params)
r.method = "POST"
r.body = buf
@@ -86,9 +106,54 @@ func (r *RequestWrapper) PostParams(params string) ResponseWrapper {
return request(r)
}
func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams map[string]string) *ResponseWrapper {
buf := &bytes.Buffer{}
// 文件写入 buf
writer := multipart.NewWriter(buf)
for _, uploadFile := range files {
var reader io.Reader
// 如果文件路径不为空则读取该路径文件否则使用bytes
if uploadFile.FilePath != "" {
file, err := os.Open(uploadFile.FilePath)
if err != nil {
return createRequestError(err)
}
defer file.Close()
reader = file
} else {
reader = bytes.NewBuffer(uploadFile.Bytes)
}
part, err := writer.CreateFormFile(uploadFile.FieldName, uploadFile.FileName)
if err != nil {
return createRequestError(err)
}
_, err = io.Copy(part, reader)
}
// 如果有其他参数则写入body
if reqParams != nil {
for k, v := range reqParams {
if err := writer.WriteField(k, v); err != nil {
return createRequestError(err)
}
}
}
if err := writer.Close(); err != nil {
return createRequestError(err)
}
r.method = "POST"
r.body = buf
if r.header == nil {
r.header = make(map[string]string)
}
r.header["Content-type"] = writer.FormDataContentType()
return request(r)
}
type ResponseWrapper struct {
StatusCode int
Body string
Body []byte
Header http.Header
}
@@ -96,28 +161,33 @@ func (r *ResponseWrapper) IsSuccess() bool {
return r.StatusCode == 200
}
func (r *ResponseWrapper) ToObj(obj interface{}) {
if !r.IsSuccess() {
return
}
_ = json.Unmarshal([]byte(r.Body), &obj)
func (r *ResponseWrapper) BodyToObj(objPtr interface{}) error {
_ = json.Unmarshal(r.Body, &objPtr)
return r.getError()
}
func (r *ResponseWrapper) ToMap() map[string]interface{} {
if !r.IsSuccess() {
return nil
}
func (r *ResponseWrapper) BodyToString() (string, error) {
return string(r.Body), r.getError()
}
func (r *ResponseWrapper) BodyToMap() (map[string]interface{}, error) {
var res map[string]interface{}
err := json.Unmarshal([]byte(r.Body), &res)
err := json.Unmarshal(r.Body, &res)
if err != nil {
return nil
return nil, err
}
return res
return res, r.getError()
}
func request(rw *RequestWrapper) ResponseWrapper {
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
client := &http.Client{}
func (r *ResponseWrapper) getError() error {
if !r.IsSuccess() {
return errors.New(string(r.Body))
}
return nil
}
func request(rw *RequestWrapper) *ResponseWrapper {
wrapper := &ResponseWrapper{StatusCode: 0, Header: make(http.Header)}
timeout := rw.timeout
if timeout > 0 {
client.Timeout = time.Duration(timeout) * time.Second
@@ -132,17 +202,17 @@ func request(rw *RequestWrapper) ResponseWrapper {
setRequestHeader(req, rw.header)
resp, err := client.Do(req)
if err != nil {
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
wrapper.Body = []byte(fmt.Sprintf("执行HTTP请求错误-%s", err.Error()))
return wrapper
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
wrapper.Body = []byte(fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error()))
return wrapper
}
wrapper.StatusCode = resp.StatusCode
wrapper.Body = string(body)
wrapper.Body = body
wrapper.Header = resp.Header
return wrapper
@@ -155,7 +225,6 @@ func setRequestHeader(req *http.Request, header map[string]string) {
}
}
func createRequestError(err error) ResponseWrapper {
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
return ResponseWrapper{0, errorMessage, make(http.Header)}
func createRequestError(err error) *ResponseWrapper {
return &ResponseWrapper{0, []byte(fmt.Sprintf("创建HTTP请求错误-%s", err.Error())), make(http.Header)}
}

View File

@@ -2,6 +2,9 @@ package logger
import (
"fmt"
"mayfly-go/base/config"
"mayfly-go/base/global"
"os"
"strings"
"time"
@@ -11,12 +14,38 @@ import (
var Log = logrus.New()
func init() {
// customFormatter := new(logrus.TextFormatter)
// customFormatter.TimestampFormat = "2006-01-02 15:04:05.000"
// customFormatter.FullTimestamp = true
Log.SetFormatter(new(LogFormatter))
Log.SetReportCaller(true)
Log.SetLevel(logrus.DebugLevel)
logConf := config.Conf.Log
// 如果不存在日志配置信息则默认debug级别
if logConf == nil {
Log.SetLevel(logrus.DebugLevel)
return
}
// 根据配置文件设置日志级别
if level := logConf.Level; level != "" {
l, err := logrus.ParseLevel(level)
if err != nil {
panic(fmt.Sprintf("日志级别不存在: %s", level))
}
Log.SetLevel(l)
} else {
Log.SetLevel(logrus.DebugLevel)
}
if logFile := logConf.File; logFile != nil {
//写入文件
file, err := os.OpenFile(logFile.GetFilename(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModeAppend|0666)
if err != nil {
panic(fmt.Sprintf("创建日志文件失败: %s", err.Error()))
}
Log.Out = file
}
global.Log = Log
}
type LogFormatter struct{}

View File

@@ -2,6 +2,7 @@ package model
import (
"fmt"
"mayfly-go/base/biz"
"mayfly-go/base/global"
"strconv"
@@ -71,22 +72,13 @@ func GetById(model interface{}, id uint64, cols ...string) error {
// 根据id列表查询
func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...string) {
var idsStr string
for i, v := range ids {
idStr := strconv.Itoa(int(v))
if i == 0 {
idsStr += idStr
} else {
idsStr += ("," + idStr)
}
}
var orderByStr string
if orderBy == nil {
orderByStr = "id desc"
} else {
orderByStr = strings.Join(orderBy, ",")
}
global.Db.Model(model).Where("id in (?)", idsStr).Order(orderByStr).Find(list)
global.Db.Model(model).Where("id in (?)", ids).Order(orderByStr).Find(list)
}
// 根据id列表查询
@@ -151,11 +143,11 @@ func GetByConditionTo(conditionModel interface{}, toModel interface{}) error {
}
// 获取分页结果
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) PageResult {
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult {
var count int64
global.Db.Model(conditionModel).Where(conditionModel).Count(&count)
if count == 0 {
return PageResult{Total: 0, List: []string{}}
return &PageResult{Total: 0, List: []string{}}
}
page := pageParam.PageNum
pageSize := pageParam.PageSize
@@ -165,12 +157,13 @@ func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interfac
} else {
orderByStr = strings.Join(orderBy, ",")
}
global.Db.Model(conditionModel).Where(conditionModel).Order(orderByStr).Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels)
return PageResult{Total: count, List: toModels}
err := global.Db.Model(conditionModel).Where(conditionModel).Order(orderByStr).Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error
biz.ErrIsNil(err, "查询失败")
return &PageResult{Total: count, List: toModels}
}
// 根据sql获取分页对象
func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...interface{}) PageResult {
func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...interface{}) *PageResult {
db := global.Db
selectIndex := strings.Index(sql, "SELECT ") + 7
fromIndex := strings.Index(sql, " FROM")
@@ -180,12 +173,13 @@ func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...int
var count int
db.Raw(countSql, args...).Scan(&count)
if count == 0 {
return PageResult{Total: 0, List: []string{}}
return &PageResult{Total: 0, List: []string{}}
}
// 分页查询
limitSql := sql + " LIMIT " + strconv.Itoa(param.PageNum-1) + ", " + strconv.Itoa(param.PageSize)
db.Raw(limitSql).Scan(toModel)
return PageResult{Total: int64(count), List: toModel}
err := db.Raw(limitSql).Scan(toModel).Error
biz.ErrIsNil(err, "查询失败")
return &PageResult{Total: int64(count), List: toModel}
}
func GetListBySql(sql string, params ...interface{}) []map[string]interface{} {
@@ -194,6 +188,6 @@ func GetListBySql(sql string, params ...interface{}) []map[string]interface{} {
return maps
}
func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) {
global.Db.Raw(sql, params).Find(toEntity)
func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) error {
return global.Db.Raw(sql, params).Find(toEntity).Error
}

View File

@@ -3,20 +3,12 @@ package model
import (
"encoding/json"
"fmt"
"mayfly-go/base/biz"
)
const (
SuccessCode = 200
SuccessMsg = "success"
BizErrorCode = 400
BizErrorMsg = "error"
ServerErrorCode = 500
ServerErrorMsg = "server error"
TokenErrorCode = 501
TokenErrorMsg = "token error"
)
// 统一返回结果结构体
@@ -52,15 +44,19 @@ func SuccessNoData() *Result {
return &Result{Code: SuccessCode, Msg: SuccessMsg}
}
// 返回服务器错误Result
func ServerError() *Result {
return &Result{Code: ServerErrorCode, Msg: ServerErrorMsg}
func Error(bizerr *biz.BizError) *Result {
return &Result{Code: bizerr.Code(), Msg: bizerr.Error()}
}
func Error(code int16, msg string) *Result {
return &Result{Code: code, Msg: msg}
// 返回服务器错误Result
func ServerError() *Result {
return Error(biz.ServerError)
}
func TokenError() *Result {
return &Result{Code: TokenErrorCode, Msg: TokenErrorMsg}
return Error(biz.PermissionErr)
}
func ErrorBy(code int16, msg string) *Result {
return &Result{Code: code, Msg: msg}
}

View File

@@ -1,6 +1,7 @@
package starter
import (
"mayfly-go/base/config"
"mayfly-go/base/global"
"gorm.io/driver/mysql"
@@ -10,7 +11,7 @@ import (
)
func GormMysql() *gorm.DB {
m := global.Config.Mysql
m := config.Conf.Mysql
if m == nil || m.Dbname == "" {
global.Log.Panic("未找到数据库配置信息")
return nil

View File

@@ -2,6 +2,7 @@ package starter
import (
"fmt"
"mayfly-go/base/config"
"mayfly-go/base/global"
"github.com/go-redis/redis"
@@ -9,7 +10,7 @@ import (
func ConnRedis() *redis.Client {
// 设置redis客户端
redisConf := global.Config.Redis
redisConf := config.Conf.Redis
if redisConf == nil {
global.Log.Panic("未找到redis配置信息")
}

View File

@@ -1,17 +1,24 @@
package starter
import (
"mayfly-go/base/config"
"mayfly-go/base/global"
"github.com/gin-gonic/gin"
)
func RunWebServer(web *gin.Engine) {
port := global.Config.Server.GetPort()
if app := global.Config.App; app != nil {
server := config.Conf.Server
port := server.GetPort()
if app := config.Conf.App; app != nil {
global.Log.Infof("%s- Listening and serving HTTP on %s", app.GetAppInfo(), port)
} else {
global.Log.Infof("Listening and serving HTTP on %s", port)
}
web.Run(port)
if server.Tls != nil && server.Tls.Enable {
web.RunTLS(port, server.Tls.CertFile, server.Tls.KeyFile)
} else {
web.Run(port)
}
}

View File

@@ -0,0 +1,21 @@
package assert
import "fmt"
// 断言条件为真不满足的panic
func IsTrue(condition bool, panicMsg string, params ...interface{}) {
if !condition {
if len(params) != 0 {
panic(fmt.Sprintf(panicMsg, params...))
}
panic(panicMsg)
}
}
func State(condition bool, panicMsg string, params ...interface{}) {
IsTrue(condition, panicMsg, params...)
}
func NotEmpty(str string, panicMsg string, params ...interface{}) {
IsTrue(str != "", panicMsg, params...)
}

View File

@@ -0,0 +1,26 @@
package utils
import (
"errors"
"strings"
"github.com/xeipuuv/gojsonschema"
)
func ValidJsonString(schemal, json string) error {
scheme, jsonLoader := gojsonschema.NewStringLoader(schemal), gojsonschema.NewStringLoader(json)
result, err := gojsonschema.Validate(scheme, jsonLoader)
if err != nil {
return err
}
if result.Valid() {
return nil
}
errs := make([]string, 0)
for _, desc := range result.Errors() {
errs = append(errs, desc.String())
}
return errors.New(strings.Join(errs, "|"))
}

View File

@@ -0,0 +1,58 @@
package utils
import (
"fmt"
"testing"
"github.com/xeipuuv/gojsonschema"
)
func TestJsonSchemal(t *testing.T) {
schema := `{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Product",
"description": "A product from Acme's catalog",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a product",
"type": "integer"
},
"name": {
"description": "Name of the product",
"type": "string"
},
"price": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
}
},
"required": ["id", "name", "price"]
}
`
json := `{"id": 1, "name": "test", "price": -21}`
err := ValidJsonString(schema, json)
fmt.Print(err)
}
func TestJs(t *testing.T) {
schemaLoader := gojsonschema.NewStringLoader(`{"type": "object","properties":{"a":{"type":"object"}},"required":["a"]}`) // json格式
documentLoader := gojsonschema.NewStringLoader(`{"a":"b"}`) // 待校验的json数据
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
panic(err.Error())
}
if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, desc := range result.Errors() {
fmt.Printf("- %s\n", desc)
}
}
}

View File

@@ -20,3 +20,8 @@ func LoadYml(path string, out interface{}) error {
}
return nil
}
func LoadYmlByString(yamlStr string, out interface{}) error {
// yaml解析
return yaml.Unmarshal([]byte(yamlStr), out)
}

23
go.mod
View File

@@ -5,20 +5,23 @@ go 1.16
require (
// jwt
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.2
github.com/go-redis/redis v6.14.2+incompatible
github.com/go-sql-driver/mysql v1.5.0
github.com/gin-gonic/gin v1.7.2
github.com/go-redis/redis v6.15.9+incompatible
github.com/gorilla/websocket v1.4.2
github.com/onsi/ginkgo v1.16.1 // indirect
github.com/onsi/gomega v1.11.0 // indirect
github.com/pkg/sftp v1.12.0
//
github.com/mojocn/base64Captcha v1.3.4
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.13.0 // indirect
github.com/pkg/sftp v1.13.1
//
github.com/robfig/cron/v3 v3.0.1
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0
github.com/sirupsen/logrus v1.6.0
github.com/sirupsen/logrus v1.8.1
// jsonschemal校验
github.com/xeipuuv/gojsonschema v1.2.0
// ssh
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
// gorm
gorm.io/driver/mysql v1.0.5
gorm.io/gorm v1.21.6
gorm.io/gorm v1.21.11
)

88
go.sum
View File

@@ -8,21 +8,23 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -31,12 +33,14 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -48,8 +52,6 @@ github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
@@ -60,73 +62,91 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mojocn/base64Captcha v1.3.4 h1:9+MZzjNSfBHniYOIpoP4xyDDPCXy14JIjsEFf89PlNw=
github.com/mojocn/base64Captcha v1.3.4/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0 h1:QIF48X1cihydXibm+4wfAc0r/qyPyuFiPFRNphdMpEE=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -140,8 +160,10 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -158,5 +180,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o=
gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.6 h1:xEFbH7WShsnAM+HeRNv7lOeyqmDAK+dDnf1AMf/cVPQ=
gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4=
gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=

View File

@@ -2,4 +2,4 @@
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = 'http://localhost:8888/api'
VITE_API_URL = 'http://api.mayflygo.1yue.net/api'

View File

@@ -18,6 +18,7 @@
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./config.js"></script>
<script type="module" src="/src/main.ts"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
</body>

View File

@@ -7,9 +7,9 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"core-js": "^3.6.5",
"axios": "^0.21.1",
"codemirror": "^5.61.0",
"core-js": "^3.6.5",
"countup.js": "^2.0.7",
"cropperjs": "^1.5.11",
"echarts": "^5.1.1",

View File

@@ -0,0 +1,3 @@
window.globalConfig = {
"BaseApiUrl": "http://localhost:8888/api"
}

View File

@@ -48,7 +48,7 @@ class Api {
* 操作该权限即请求对应的url
* @param {Object} param 请求该权限的参数
*/
request(param: any): Promise<any> {
request(param: any = null): Promise<any> {
return request.send(this, param);
}

View File

@@ -3,7 +3,7 @@
*/
class AssertError extends Error {
constructor(message: string) {
super(message); // (1)
super(message);
// 错误类名
this.name = "AssertError";
}

View File

@@ -1,5 +1,5 @@
const config = {
baseApiUrl: import.meta.env.VITE_API_URL
baseApiUrl: (window as any).globalConfig.BaseApiUrl
}
export default config

View File

@@ -2,7 +2,7 @@ import request from './request'
export default {
login: (param: any) => request.request('POST', '/sys/accounts/login', param, null),
captcha: () => request.request('GET', '/open/captcha', null, null),
captcha: () => request.request('GET', '/sys/captcha', null, null),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param, null),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param, null)
}

View File

@@ -53,6 +53,14 @@ export default defineComponent({
type: String,
default: null,
},
height: {
type: String,
default: "500px",
},
width: {
type: String,
default: "auto",
},
canChangeMode: {
type: Boolean,
default: false,
@@ -165,15 +173,14 @@ export default defineComponent({
}
);
watch(
() => props.options,
(newValue, oldValue) => {
console.log('options change', newValue);
for (const key in newValue) {
coder.setOption(key, newValue[key]);
}
}
);
// watch(
// () => props.options,
// (newValue, oldValue) => {
// for (const key in newValue) {
// coder.setOption(key, newValue[key]);
// }
// }
// );
const init = () => {
if (props.options) {
@@ -195,6 +202,9 @@ export default defineComponent({
}
});
coder.setSize(props.width, props.height);
// editor.setSize('width','height');
// 修改编辑器的语法配置
setMode(language.value);
@@ -285,6 +295,7 @@ export default defineComponent({
coder.setValue(newVal);
state.content = newVal;
coder.scrollTo(scrollInfo.left, scrollInfo.top);
refresh()
}
};
@@ -292,6 +303,7 @@ export default defineComponent({
...toRefs(state),
textarea,
changeMode,
refresh,
};
},
});

View File

@@ -85,7 +85,7 @@ export default defineComponent({
});
const submit = () => {
dynamicForm.validate((valid: boolean) => {
dynamicForm.value.validate((valid: boolean) => {
if (valid) {
// 提交的表单数据
const subform = { ...state.form };

View File

@@ -8,5 +8,9 @@ export const imports = {
"ResourceList": () => import('@/views/system/resource'),
"RoleList": () => import('@/views/system/role'),
"AccountList": () => import('@/views/system/account'),
"SelectData": () => import('@/views/ops/db'),
"ProjectList": () => import('@/views/ops/project/ProjectList.vue'),
"DbList": () => import('@/views/ops/db/DbList.vue'),
"SqlExec": () => import('@/views/ops/db'),
"RedisList": () => import('@/views/ops/redis'),
"DataOperation": () => import('@/views/ops/redis/DataOperation.vue'),
}

View File

@@ -14,8 +14,8 @@ body,
padding: 0;
width: 100%;
height: 100%;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
font-weight: 500;
font-family: Microsoft YaHei, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, SimSun, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
@@ -274,7 +274,7 @@ body,
.toolbar {
width: 100%;
padding: 8px;
padding: 6px;
background-color: #ffffff;
overflow: hidden;
line-height: 32px;
@@ -283,4 +283,10 @@ body,
.fl {
float: left;
}
.search-form {
.el-form-item {
margin-bottom: 3px;
}
}

View File

@@ -4,10 +4,10 @@
<div class="left">
<div class="left-item">
<div class="left-item-animation left-item-num">401</div>
<div class="left-item-animation left-item-title">您未被授权没有操作权限</div>
<div class="left-item-animation left-item-title">您未被授权或登录超时没有操作权限</div>
<div class="left-item-animation left-item-msg"></div>
<div class="left-item-animation left-item-btn">
<el-button type="primary" round @click="onSetAuth">重新授权</el-button>
<el-button type="primary" round @click="onSetAuth">重新登录</el-button>
</div>
</div>
</div>

View File

@@ -7,8 +7,8 @@
<img :src="getUserInfos.photo" />
<div class="home-card-first-right ml15">
<div class="flex-margin">
<div class="home-card-first-right-title">{{ currentTime }}admin</div>
<div class="home-card-first-right-msg mt5">超级管理</div>
<div class="home-card-first-right-title">{{ `${currentTime}, ${getUserInfos.username}` }}</div>
<!-- <div class="home-card-first-right-msg mt5">超级管理</div> -->
</div>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<template>
<el-form class="login-content-form">
<el-form-item>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form">
<el-form-item prop="username">
<el-input type="text" placeholder="请输入用户名" prefix-icon="el-icon-user" v-model="loginForm.username" clearable autocomplete="off">
</el-input>
</el-form-item>
<el-form-item>
<el-form-item prop="password">
<el-input
type="password"
placeholder="请输入密码"
@@ -15,29 +15,36 @@
>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item prop="captcha">
<el-row :gutter="15">
<el-col :span="16">
<el-input
type="text"
maxlength="4"
maxlength="6"
placeholder="请输入验证码"
prefix-icon="el-icon-position"
v-model="loginForm.code"
v-model="loginForm.captcha"
clearable
autocomplete="off"
@keyup.enter="onSignIn"
@keyup.enter="login"
></el-input>
</el-col>
<el-col :span="8">
<div class="login-content-code">
<span class="login-content-code-img">1234</span>
<img
class="login-content-code-img"
@click="getCaptcha"
width="130px"
height="40px"
:src="captchaImage"
style="cursor: pointer"
/>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-content-submit" round @click="onSignIn" :loading="loading.signIn">
<el-button type="primary" class="login-content-submit" round @click="login" :loading="loading.signIn">
<span> </span>
</el-button>
</el-form-item>
@@ -45,7 +52,7 @@
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed } from 'vue';
import { onMounted, ref, toRefs, reactive, defineComponent, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initAllFun, initBackEndControlRoutesFun } from '@/router/index.ts';
@@ -60,22 +67,51 @@ export default defineComponent({
const store = useStore();
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
const state = reactive({
captchaImage: '',
loginForm: {
username: 'test',
password: '123456',
code: '1234',
captcha: '',
cid: '',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
},
loading: {
signIn: false,
},
});
onMounted(() => {
getCaptcha();
});
const getCaptcha = async () => {
let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid;
};
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 校验登录表单并登录
const login = () => {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
onSignIn();
} else {
return false;
}
});
};
// 登录
const onSignIn = async () => {
state.loading.signIn = true;
@@ -87,6 +123,8 @@ export default defineComponent({
setSession('menus', loginRes.menus);
} catch (e) {
state.loading.signIn = false;
state.loginForm.captcha = '';
getCaptcha();
return;
}
// 用户信息模拟数据
@@ -132,9 +170,12 @@ export default defineComponent({
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
}, 300);
};
return {
getCaptcha,
currentTime,
onSignIn,
loginFormRef,
login,
...toRefs(state),
};
},

View File

@@ -0,0 +1,84 @@
<template>
<div>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px" size="small">
<el-form-item prop="project" label="项目" label-width="40px">
<el-select v-model="projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="env" label="环境" label-width="40px">
<el-select style="width: 100px" v-model="envId" placeholder="环境" @change="changeEnv" filterable>
<el-option v-for="item in envs" :key="item.id" :label="item.name" :value="item.id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.remark }}</span>
</el-option>
</el-select>
</el-form-item>
<slot></slot>
</el-form>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, watch, defineComponent, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { notEmpty } from '@/common/assert';
import { projectApi } from '../project/api';
export default defineComponent({
name: 'ProjectEnvSelect',
props: {
visible: {
type: Boolean,
},
data: {
type: Object,
},
title: {
type: String,
},
machineId: {
type: Number,
},
isCommon: {
type: Boolean,
},
},
setup(props: any, { emit }) {
const state = reactive({
projects: [] as any,
envs: [] as any,
projectId: null,
envId: null,
});
watch(props, (newValue, oldValue) => {});
onMounted(async () => {
state.projects = await projectApi.accountProjects.request(null);
});
const changeProject = async (projectId: any) => {
emit('update:projectId', projectId);
emit('changeProjectEnv', state.projectId, null);
state.envId = null;
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeEnv = (envId: any) => {
emit('update:envId', envId);
emit('changeProjectEnv', state.projectId, envId);
};
return {
...toRefs(state),
changeProject,
changeEnv,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,240 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.trim="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-input v-model.trim="form.database" placeholder="请输入数据库名"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { dbApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'DbEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
db: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const dbForm: any = ref(null);
const state = reactive({
visible: false,
projects: [],
envs: [],
form: {
id: null,
name: null,
port: 3306,
username: null,
password: null,
project: null,
projectId: null,
envId: null,
env: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
type: [
{
required: true,
message: '请选择数据库类型',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
database: [
{
required: true,
message: '请输入数据库名',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
state.projects = newValue.projects;
if (newValue.db) {
getEnvs(newValue.db.projectId);
state.form = { ...newValue.db };
} else {
state.envs = [];
state.form = { port: 3306 } as any;
}
});
const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.project = p.name;
}
}
state.envs = [];
getEnvs(projectId);
};
const changeEnv = (envId: number) => {
for (let p of state.envs as any) {
if (p.id == envId) {
state.form.env = p.name;
}
}
};
const btnOk = async () => {
dbForm.value.validate((valid: boolean) => {
if (valid) {
dbApi.saveDb.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
dbForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
dbForm,
changeProject,
changeEnv,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="db-list">
<div class="toolbar">
<el-row>
<el-col>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px" size="small">
<el-form-item prop="project" label="项目">
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数据库">
<el-input v-model="query.database" auto-complete="off" clearable></el-input>
</el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="search()">查询</el-button>
</el-form>
</el-col>
</el-row>
<el-row class="mt5">
<el-col>
<el-button v-auth="permissions.saveDb" type="primary" icon="el-icon-plus" size="mini" @click="editDb(true)">添加</el-button>
<el-button
v-auth="permissions.saveDb"
:disabled="chooseId == null"
@click="editDb(false)"
type="primary"
icon="el-icon-edit"
size="mini"
>编辑</el-button
>
<el-button
v-auth="permissions.delDb"
:disabled="chooseId == null"
@click="deleteDb(chooseId)"
type="danger"
icon="el-icon-delete"
size="mini"
>删除</el-button
>
</el-col>
</el-row>
</div>
<el-table :data="datas" border ref="table" @current-change="choose" show-overflow-tooltip>
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="project" label="项目" min-width="100"></el-table-column>
<el-table-column prop="env" label="环境" min-width="100"></el-table-column>
<el-table-column prop="name" label="名称" min-width="200"></el-table-column>
<el-table-column min-width="160" label="host:port">
<template #default="scope">
{{ `${scope.row.host}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column prop="type" label="类型" min-width="80"></el-table-column>
<el-table-column prop="database" label="数据库" min-width="120"></el-table-column>
<el-table-column prop="username" label="用户名" min-width="100"></el-table-column>
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
<el-table-column min-width="160" prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<db-edit @val-change="valChange" :projects="projects" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div>
</template>
<script lang='ts'>
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
import DbEdit from './DbEdit.vue';
import { dbApi } from './api';
import { projectApi } from '../project/api.ts';
export default defineComponent({
name: 'DbList',
components: {
ProjectEnvSelect,
DbEdit,
},
setup() {
const state = reactive({
permissions: {
saveDb: 'db:save',
delDb: 'db:del',
},
projects: [],
chooseId: null,
/**
* 选中的数据
*/
chooseData: null,
/**
* 查询条件
*/
query: {
pageNum: 1,
pageSize: 10,
},
datas: [],
total: 0,
dbEditDialog: {
visible: false,
data: null,
title: '新增数据库',
},
});
onMounted(async () => {
search();
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
});
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const search = async () => {
let res: any = await dbApi.dbs.request(state.query);
state.datas = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const editDb = (isAdd = false) => {
if (isAdd) {
state.dbEditDialog.data = null;
state.dbEditDialog.title = '新增数据库';
} else {
state.dbEditDialog.data = state.chooseData;
state.dbEditDialog.title = '修改数据库';
}
state.dbEditDialog.visible = true;
};
const valChange = () => {
search();
};
const deleteDb = async (id: number) => {
try {
await ElMessageBox.confirm(`确定删除该库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) {}
};
return {
...toRefs(state),
// enums,
search,
choose,
handlePageChange,
editDb,
valChange,
deleteDb,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -1,15 +1,28 @@
<template>
<div>
<div class="toolbar">
<div class="fl">
<el-select size="small" v-model="dbId" placeholder="请选择数据库" @change="changeDb" @clear="clearDb" clearable filterable>
<el-option v-for="item in dbs" :key="item.id" :label="`${item.name} [${dbTypeName(item.type)}]`" :value="item.id"> </el-option>
</el-select>
</div>
<el-row type="flex" justify="space-between">
<el-col :span="24">
<project-env-select @changeProjectEnv="changeProjectEnv" @clear="clearDb">
<template #default>
<el-form-item label="数据库">
<el-select v-model="dbId" placeholder="请选择数据库" @change="changeDb" @clear="clearDb" clearable filterable>
<el-option v-for="item in dbs" :key="item.id" :label="item.database" :value="item.id">
<span style="float: left">{{ item.database }}</span>
<span style="float: right; color: #8492a6; margin-left: 6px; font-size: 13px">{{
`${item.name} [${item.type}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
</template>
</project-env-select>
</el-col>
</el-row>
</div>
<el-container style="height: 50%; border: 1px solid #eee; margin-top: 1px">
<el-aside width="70%" style="background-color: rgb(238, 241, 246)">
<el-container style="border: 1px solid #eee; margin-top: 1px">
<el-aside id="sqlcontent" width="65%" style="background-color: rgb(238, 241, 246)">
<div class="toolbar">
<div class="fl">
<el-button @click="runSql" type="success" icon="el-icon-video-play" size="mini" plain>执行</el-button>
@@ -19,12 +32,12 @@
<el-button @click="saveSql" type="primary" icon="el-icon-document-add" size="mini" plain>保存</el-button>
</div>
</div>
<codemirror class="codesql" ref="cmEditor" language="sql" v-model="sql" :options="cmOptions" />
<codemirror @beforeChange="onBeforeChange" class="codesql" ref="cmEditor" language="sql" v-model="sql" :options="cmOptions" />
</el-aside>
<el-container style="margin-left: 2px">
<el-header style="text-align: left; height: 45px; font-size: 12px; padding: 0px">
<el-select v-model="tableName" placeholder="请选择表" @change="changeTable" clearable filterable style="width: 99%">
<el-select v-model="tableName" placeholder="请选择表" @change="changeTable" filterable style="width: 99%">
<el-option
v-for="item in tableMetadata"
:key="item.tableName"
@@ -35,20 +48,22 @@
</el-select>
</el-header>
<el-main style="padding: 0px; height: 100%; overflow: hidden">
<el-main style="padding: 0px; overflow: hidden">
<el-table :data="columnMetadata" height="100%" size="mini">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
<el-table style="margin-top: 1px" :data="selectRes.data" size="mini" max-height="300" stripe border>
<el-table style="margin-top: 1px" :data="execRes.data" size="mini" max-height="300" :empty-text="execRes.emptyResText" stripe border>
<el-table-column
min-width="92"
min-width="100"
:width="flexColumnWidth(item, execRes.data)"
align="center"
v-for="item in selectRes.tableColumn"
v-for="item in execRes.tableColumn"
:key="item"
:prop="item"
:label="item"
@@ -56,20 +71,11 @@
>
</el-table-column>
</el-table>
<!-- <el-pagination
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="data.total"
:current-page.sync="params.pageNum"
:page-size="params.pageSize"
/> -->
</div>
</template>
<script lang="ts">
import { toRefs, reactive, computed, onMounted, defineComponent, ref } from 'vue';
import { toRefs, reactive, computed, defineComponent, ref } from 'vue';
import { dbApi } from './api';
import 'codemirror/theme/ambiance.css';
@@ -79,48 +85,56 @@ import 'codemirror/lib/codemirror.css';
// options
import 'codemirror/theme/base16-light.css';
// require('codemirror/addon/edit/matchbrackets')
import 'codemirror/addon/selection/active-line';
import { codemirror } from '@/components/codemirror';
// import 'codemirror/mode/sql/sql.js';
// import 'codemirror/addon/hint/show-hint.js';
// import 'codemirror/addon/hint/sql-hint.js';
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/addon/hint/sql-hint.js';
import sqlFormatter from 'sql-formatter';
import { notEmpty } from '@/common/assert';
import { notNull, notEmpty } from '@/common/assert';
import { ElMessage } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
export default defineComponent({
name: 'SelectData',
name: 'SqlExec',
components: {
codemirror,
ProjectEnvSelect,
},
setup() {
const cmEditor: any = ref(null);
const state = reactive({
dbs: [],
tables: [],
dbId: '',
dbId: null,
tableName: '',
tableMetadata: [],
columnMetadata: [],
sql: '',
selectRes: {
sqlTabs: {
tabs: [] as any,
active: '',
index: 1,
},
execRes: {
tableColumn: [],
data: [],
emptyResText: '没有数据',
},
params: {
pageNum: 1,
pageSize: 10,
envId: null,
},
cmOptions: {
tabSize: 4,
mode: 'text/x-sql',
// theme: 'cobalt',
lineNumbers: true,
line: true,
indentWithTabs: true,
smartIndent: true,
// matchBrackets: true,
matchBrackets: true,
theme: 'base16-light',
autofocus: true,
extraKeys: { Tab: 'autocomplete' }, //
@@ -137,14 +151,19 @@ export default defineComponent({
return cmEditor.value.coder;
});
const dbTypeName = (type: any) => {
return 'mysql';
/**
* 项目及环境更改后的回调事件
*/
const changeProjectEnv = (projectId: any, envId: any) => {
state.dbs = [];
state.dbId = null;
clearDb();
if (envId != null) {
state.params.envId = envId;
search();
}
};
onMounted(() => {
search();
});
/**
* 输入字符给提示
*/
@@ -154,29 +173,92 @@ export default defineComponent({
}
};
const onBeforeChange = (instance: any, changeObj: any) => {
var text = changeObj.text[0];
// sql
changeObj.text[0] = text.split(' ')[0];
};
/**
* 执行sql
*/
const runSql = async () => {
notEmpty(state.dbId, '请先选择数据库');
notNull(state.dbId, '请先选择数据库');
//
let selectSql = getSql();
notEmpty(selectSql, '内容不能为空');
const res = await dbApi.selectData.request({
let sql = getSql();
notNull(sql, '内容不能为空');
state.execRes.tableColumn = [];
state.execRes.data = [];
state.execRes.emptyResText = '查询中...';
const res = await dbApi.sqlExec.request({
id: state.dbId,
selectSql: selectSql,
sql: sql,
});
let tableColumn: any;
let data;
if (res.length > 0) {
tableColumn = Object.keys(res[0]);
data = res;
} else {
tableColumn = [];
data = [];
state.execRes.emptyResText = '没有数据';
state.execRes.tableColumn = res.colNames;
state.execRes.data = res.res;
};
const flexColumnWidth = (str: any, tableData: any, flag = 'equal') => {
// str();tableData();
// flag,'max''equal','max'
// flag'max',flag'equal'
str = str + '';
let columnContent = '';
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return;
}
state.selectRes.tableColumn = tableColumn;
state.selectRes.data = data;
if (!str || !str.length || str.length === 0 || str === undefined) {
return;
}
if (flag === 'equal') {
// ()
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str].length > 0) {
columnContent = tableData[i][str];
break;
}
}
} else {
// ()
let index = 0;
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str] === null) {
return;
}
const now_temp = tableData[i][str] + '';
const max_temp = tableData[index][str] + '';
if (now_temp.length > max_temp.length) {
index = i;
}
}
columnContent = tableData[index][str];
}
//
let flexWidth = 0;
for (const char of columnContent) {
if ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z')) {
// 8
flexWidth += 8;
} else if (char >= '\u4e00' && char <= '\u9fa5') {
// 15
flexWidth += 15;
} else {
// 8
flexWidth += 8;
}
}
if (flexWidth < 80) {
//
flexWidth = 80;
}
if (flexWidth > 350) {
//
flexWidth = 350;
}
return flexWidth + 'px';
};
/**
@@ -193,7 +275,7 @@ export default defineComponent({
const saveSql = async () => {
notEmpty(state.sql, 'sql内容不能为空');
notEmpty(state.dbId, '请先选择数据库');
notNull(state.dbId, '请先选择数据库');
await dbApi.saveSql.request({ id: state.dbId, sql: state.sql, type: 1 });
ElMessage.success('保存成功');
};
@@ -235,9 +317,10 @@ export default defineComponent({
state.tableName = '';
state.tableMetadata = [];
state.columnMetadata = [];
state.selectRes.data = [];
state.selectRes.tableColumn = [];
state.execRes.data = [];
state.execRes.tableColumn = [];
state.sql = '';
state.cmOptions.hintOptions.tables = [];
};
//
@@ -268,7 +351,7 @@ export default defineComponent({
codemirror.value.replaceSelection(sqlFormatter.format(selectSql));
} else {
/* 将sql内容进行格式后放入编辑器中*/
codemirror.value.setValue(sqlFormatter.format(state.sql));
state.sql = sqlFormatter.format(sqlFormatter.format(state.sql));
}
};
@@ -280,14 +363,16 @@ export default defineComponent({
return {
...toRefs(state),
cmEditor,
dbTypeName,
changeProjectEnv,
inputRead,
changeTable,
runSql,
flexColumnWidth,
saveSql,
changeDb,
clearDb,
formatSql,
onBeforeChange,
};
},
});
@@ -298,4 +383,9 @@ export default defineComponent({
font-size: 10pt;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
}
#sqlcontent {
.CodeMirror {
height: 300px !important;
}
}
</style>

View File

@@ -3,11 +3,13 @@ import Api from '@/common/Api';
export const dbApi = {
// 获取权限列表
dbs: Api.create("/dbs", 'get'),
saveDb: Api.create("/dbs", 'post'),
deleteDb: Api.create("/dbs/{id}", 'delete'),
tableMetadata: Api.create("/dbs/{id}/t-metadata", 'get'),
columnMetadata: Api.create("/dbs/{id}/c-metadata", 'get'),
// 获取表即列提示
hintTables: Api.create("/dbs/{id}/hint-tables", 'get'),
selectData: Api.create("/dbs/{id}/select", 'get'),
sqlExec: Api.create("/dbs/{id}/exec-sql", 'get'),
// 保存sql
saveSql: Api.create("/dbs/{id}/sql", 'post'),
// 获取保存的sql

View File

@@ -1 +1 @@
export { default } from './SelectData.vue';
export { default } from './SqlExec.vue';

View File

@@ -0,0 +1,182 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="machineForm" :rules="rules" label-width="85px" size="small">
<!-- <el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item> -->
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-input v-model.trim="form.ip" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.trim="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, onMounted, defineComponent, ref } from 'vue';
import { machineApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'MachineEdit',
props: {
visible: {
type: Boolean,
},
machine: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const machineForm: any = ref(null);
const state = reactive({
visible: false,
form: {
id: null,
name: null,
port: 22,
username: null,
password: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
ip: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
if (newValue.machine) {
state.form = { ...newValue.machine };
} else {
state.form = { port: 22 } as any;
}
});
const btnOk = async () => {
machineForm.value.validate((valid: boolean) => {
if (valid) {
machineApi.saveMachine.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
machineForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
machineForm,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -42,16 +42,19 @@
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column>
<el-table-column prop="ip" label="IP" width></el-table-column>
<el-table-column prop="port" label="端口" :min-width="40"></el-table-column>
<el-table-column prop="username" label="用户名" :min-width="40"></el-table-column>
<el-table-column prop="createTime" label="创建时间" :min-width="100">
<el-table-column prop="ip" label="ip:port" min-width="160">
<template #default="scope">
{{ `${scope.row.ip}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" :min-width="45"></el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者" :min-width="50"></el-table-column>
<el-table-column prop="updateTime" label="更新时间" :min-width="100">
<el-table-column prop="creator" label="创建者" min-width="50"></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="160">
<template #default="scope">
{{ $filters.dateFormat(scope.row.updateTime) }}
</template>
@@ -59,7 +62,7 @@
<el-table-column prop="modifier" label="修改者" :min-width="50"></el-table-column>
<el-table-column label="操作" min-width="200px">
<template #default="scope">
<el-button type="primary" @click="monitor(scope.row.id)" icom="el-icon-tickets" size="mini" plain>监控</el-button>
<!-- <el-button type="primary" @click="monitor(scope.row.id)" icom="el-icon-tickets" size="mini" plain>监控</el-button> -->
<el-button type="success" @click="serviceManager(scope.row)" size="mini" plain>脚本管理</el-button>
<el-button v-auth="'machine:terminal'" type="success" @click="showTerminal(scope.row)" size="mini" plain>终端</el-button>
</template>
@@ -75,6 +78,12 @@
:page-size="params.pageSize"
/>
<machine-edit
:title="machineEditDialog.title"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
></machine-edit>
<!-- <el-dialog @close="closeMonitor" title="监控信息" v-model="monitorDialog.visible" width="60%">
<monitor ref="monitorDialogRef" :machineId="monitorDialog.machineId" />
</el-dialog> -->
@@ -82,40 +91,32 @@
<service-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<dynamic-form-dialog
v-model:visible="formDialog.visible"
:title="formDialog.title"
:formInfo="formDialog.formInfo"
v-model:formData="formDialog.formData"
@submitSuccess="submitSuccess"
></dynamic-form-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { DynamicFormDialog } from '@/components/dynamic-form';
// import Monitor from './Monitor.vue';
import { machineApi } from './api';
import SshTerminal from './SshTerminal.vue';
import ServiceManage from './ServiceManage.vue';
import FileManage from './FileManage.vue';
import MachineEdit from './MachineEdit.vue';
export default defineComponent({
name: 'MachineList',
components: {
// Monitor,
SshTerminal,
ServiceManage,
FileManage,
DynamicFormDialog,
MachineEdit,
},
setup() {
const router = useRouter();
// const monitorDialogRef = ref();
const state = reactive({
params: {
pageNum: 1,
@@ -149,86 +150,10 @@ export default defineComponent({
visible: false,
machineId: 0,
},
formDialog: {
machineEditDialog: {
visible: false,
title: '',
formInfo: {
createApi: machineApi.save,
updateApi: machineApi.save,
formRows: [
[
{
type: 'input',
label: '名称:',
name: 'name',
placeholder: '请输入名称',
rules: [
{
required: true,
message: '请输入名称',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: 'ip',
name: 'ip',
placeholder: '请输入ip',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '端口号:',
name: 'port',
placeholder: '请输入端口号',
inputType: 'number',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '用户名:',
name: 'username',
placeholder: '请输入用户名',
rules: [
{
required: true,
message: '请输入用户名',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '密码:',
name: 'password',
placeholder: '请输入密码',
inputType: 'password',
},
],
],
},
formData: { port: 22 },
data: null,
title: '新增机器',
},
});
@@ -261,7 +186,6 @@ export default defineComponent({
// };
const showTerminal = (row: any) => {
// router.push(`/machine/${row.id}/terminal?id=${row.id}&name=${row.name}&time=${new Date().getTime()}`);
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
@@ -275,21 +199,30 @@ export default defineComponent({
const openFormDialog = (redis: any) => {
let dialogTitle;
if (redis) {
state.formDialog.formData = state.currentData as any;
state.machineEditDialog.data = state.currentData as any;
dialogTitle = '编辑机器';
} else {
state.formDialog.formData = { port: 22 };
state.machineEditDialog.data = { port: 22 } as any;
dialogTitle = '添加机器';
}
state.formDialog.title = dialogTitle;
state.formDialog.visible = true;
state.machineEditDialog.title = dialogTitle;
state.machineEditDialog.visible = true;
};
const deleteMachine = async (id: number) => {
await machineApi.del.request({ id });
ElMessage.success('操作成功');
search();
try {
await ElMessageBox.confirm(`确定删除该机器信息? 该操作将同时删除脚本及文件配置信息`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.del.request({ id });
ElMessage.success('操作成功');
state.currentId = null;
state.currentData = null;
search();
} catch (err) {}
};
const serviceManager = (row: any) => {

View File

@@ -24,6 +24,10 @@
</el-select>
</el-form-item>
<el-form-item prop="params" label="参数">
<el-input v-model.trim="form.params" placeholder="参数数组json若无可不填"></el-input>
</el-form-item>
<el-form-item prop="script" label="内容" id="content">
<codemirror ref="cmEditor" v-model="form.script" language="shell" />
</el-form-item>
@@ -31,7 +35,15 @@
<template #footer>
<div class="dialog-footer">
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" size="mini" :disabled="submitDisabled"> </el-button>
<el-button
v-auth="'machine:script:save'"
type="primary"
:loading="btnLoading"
@click="btnOk"
size="mini"
:disabled="submitDisabled"
> </el-button
>
<el-button @click="cancel()" :disabled="submitDisabled" size="mini"> </el-button>
</div>
</template>
@@ -83,6 +95,7 @@ export default defineComponent({
machineId: 0,
description: '',
script: '',
params: null,
type: null,
},
btnLoading: false,

View File

@@ -36,7 +36,7 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" :min-width="50"> </el-table-column>
<el-table-column prop="name" label="名称" :min-width="70"> </el-table-column>
<el-table-column prop="description" label="描述" :min-width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="name" label="类型" :min-width="50">
<template #default="scope">
@@ -64,6 +64,19 @@
</el-table>
</el-dialog>
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="mini">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="hasParamsRun(currentData)" size="mini"> </el-button>
</span>
</template>
</el-dialog>
<el-dialog title="执行结果" v-model="resultDialog.visible" width="40%">
<div style="white-space: pre-line; padding: 10px; color: #000000">
{{ resultDialog.result }}
@@ -94,7 +107,7 @@
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent } from 'vue';
import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import SshTerminal from './SshTerminal.vue';
import { machineApi } from './api';
@@ -113,6 +126,7 @@ export default defineComponent({
title: { type: String },
},
setup(props: any, context) {
const paramsForm: any = ref(null);
const state = reactive({
visible: false,
type: 0,
@@ -125,6 +139,11 @@ export default defineComponent({
machineId: 9999999,
},
scriptTable: [],
scriptParamsDialog: {
visible: false,
params: {},
paramsFormItem: [],
},
resultDialog: {
visible: false,
result: '',
@@ -152,13 +171,43 @@ export default defineComponent({
};
const runScript = async (script: any) => {
// 如果存在参数,则弹窗输入参数后执行
if (script.params) {
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
state.scriptParamsDialog.visible = true;
return;
}
run(script);
};
// 有参数的脚本执行函数
const hasParamsRun = async (script: any) => {
// 如果脚本参数弹窗显示,则校验参数表单数据通过后执行
if (state.scriptParamsDialog.visible) {
paramsForm.value.validate((valid: any) => {
if (valid) {
run(script);
state.scriptParamsDialog.params = {};
state.scriptParamsDialog.visible = false;
paramsForm.value.resetFields();
} else {
return false;
}
});
}
};
const run = async (script: any) => {
const noResult = script.type == enums.scriptTypeEnum['NO_RESULT'].value;
// 如果脚本类型为有结果类型,则显示结果信息
if (script.type == enums.scriptTypeEnum['RESULT'].value || noResult) {
const res = await machineApi.runScript.request({
machineId: props.machineId,
scriptId: script.id,
params: state.scriptParamsDialog.params,
});
if (noResult) {
ElMessage.success('执行完成');
return;
@@ -241,9 +290,11 @@ export default defineComponent({
return {
...toRefs(state),
paramsForm,
enums,
getScripts,
runScript,
hasParamsRun,
closeTermnial,
choose,
editScript,

View File

@@ -6,9 +6,9 @@ export const machineApi = {
info: Api.create("/machines/{id}/sysinfo", 'get'),
top: Api.create("/machines/{id}/top", 'get'),
// 保存按钮
save: Api.create("/machines", 'post'),
saveMachine: Api.create("/machines", 'post'),
// 删除机器
del: Api.create("/devops/machines/{id}", 'delete'),
del: Api.create("/machines/delete/{id}", 'delete'),
scripts: Api.create("/machines/{machineId}/scripts", 'get'),
runScript: Api.create("/machines/{machineId}/scripts/{scriptId}/run", 'get'),
saveScript: Api.create("/machines/{machineId}/scripts", 'post'),

View File

@@ -0,0 +1,413 @@
<template>
<div class="project-list">
<div class="toolbar">
<el-button @click="showAddProjectDialog" v-auth="permissions.saveProject" type="primary" icon="el-icon-plus" size="mini">添加</el-button>
<el-button
@click="showAddProjectDialog(chooseData)"
v-auth="permissions.saveProject"
:disabled="chooseId == null"
type="primary"
icon="el-icon-edit"
size="mini"
>编辑</el-button
>
<el-button @click="showMembers(chooseData)" :disabled="chooseId == null" type="success" icon="el-icon-setting" size="mini"
>成员管理</el-button
>
<el-button @click="showEnv(chooseData)" :disabled="chooseId == null" type="info" icon="el-icon-setting" size="mini">环境管理</el-button>
<el-button v-auth="'role:del'" :disabled="chooseId == null" type="danger" icon="el-icon-delete" size="mini">删除</el-button>
<div style="float: right">
<el-input
class="mr2"
placeholder="请输入项目名!"
size="small"
style="width: 140px"
v-model="query.name"
@clear="search"
clearable
></el-input>
<el-button @click="search" type="success" icon="el-icon-search" size="mini"></el-button>
</div>
</div>
<el-table :data="projects" @current-change="choose" border ref="table" style="width: 100%">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="项目名"></el-table-column>
<el-table-column prop="remark" label="描述" min-width="180px" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者"> </el-table-column>
<!-- <el-table-column label="查看更多" min-width="80px">
<template #default="scope">
<el-link @click.prevent="showMembers(scope.row)" type="success">成员</el-link>
<el-link class="ml5" @click.prevent="showEnv(scope.row)" type="info">环境</el-link>
</template>
</el-table-column> -->
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<el-dialog width="400px" title="项目编辑" :before-close="cancelAddProject" v-model="addProjectDialog.visible">
<el-form :model="addProjectDialog.form" size="small" label-width="70px">
<el-form-item label="项目名:" required>
<el-input :disabled="addProjectDialog.form.id" v-model="addProjectDialog.form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="描述:">
<el-input v-model="addProjectDialog.form.remark" auto-complete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addProject" type="primary" size="small"> </el-button>
<el-button @click="cancelAddProject()" size="small"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog width="500px" :title="showEnvDialog.title" v-model="showEnvDialog.visible">
<div class="toolbar">
<el-button @click="showAddEnvDialog" v-auth="permissions.saveMember" type="primary" icon="el-icon-plus" size="mini">添加</el-button>
<!-- <el-button v-auth="'role:update'" :disabled="chooseId == null" type="danger" icon="el-icon-delete" size="mini">删除</el-button> -->
</div>
<el-table border :data="showEnvDialog.envs" size="small">
<el-table-column property="name" label="环境名" width="125"></el-table-column>
<el-table-column property="remark" label="描述" width="125"></el-table-column>
<el-table-column property="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-dialog width="400px" title="添加环境" :before-close="cancelAddEnv" v-model="showEnvDialog.addVisible">
<el-form :model="showEnvDialog.envForm" size="small" label-width="70px">
<el-form-item label="环境名:" required>
<el-input v-model="showEnvDialog.envForm.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button v-auth="permissions.saveEnv" @click="addEnv" type="primary" :loading="btnLoading" size="small"> </el-button>
<el-button @click="cancelAddEnv()" size="small"> </el-button>
</div>
</template>
</el-dialog>
</el-dialog>
<el-dialog width="500px" :title="showMemDialog.title" v-model="showMemDialog.visible">
<div class="toolbar">
<el-button v-auth="permissions.saveMember" @click="showAddMemberDialog()" type="primary" icon="el-icon-plus" size="mini"
>添加</el-button
>
<el-button
v-auth="permissions.delMember"
@click="deleteMember"
:disabled="showMemDialog.chooseId == null"
type="danger"
icon="el-icon-delete"
size="mini"
>移除</el-button
>
</div>
<el-table @current-change="chooseMember" border :data="showMemDialog.members.list" size="small">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="showMemDialog.chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column property="username" label="账号" width="125"></el-table-column>
<el-table-column property="createTime" label="加入时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column property="creator" label="分配者" width="125"></el-table-column>
</el-table>
<el-pagination
@current-change="setMemebers"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="showMemDialog.members.total"
v-model:current-page="showMemDialog.query.pageNum"
:page-size="showMemDialog.query.pageSize"
/>
<el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible">
<el-form :model="showMemDialog.memForm" size="small" label-width="70px">
<el-form-item label="账号:">
<el-select style="width: 100%" remote :remote-method="getAccount" v-model="showMemDialog.memForm.accountId" filterable placeholder="请选择">
<el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button v-auth="permissions.saveMember" @click="addMember" type="primary" :loading="btnLoading" size="small"
> </el-button
>
<el-button @click="cancelAddMember()" size="small"> </el-button>
</div>
</template>
</el-dialog>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { projectApi } from './api';
import { accountApi } from '../../system/api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { notEmpty, notNull } from '@/common/assert';
import { auth } from '../../../common/utils/authFunction';
export default defineComponent({
name: 'ProjectList',
components: {},
setup() {
const state = reactive({
permissions: {
saveProject: 'project:save',
saveMember: 'project:member:add',
delMember: 'project:member:del',
saveEnv: 'project:env:add',
},
query: {
pageNum: 1,
pageSize: 10,
name: null,
},
total: 0,
projects: [],
btnLoading: false,
chooseId: null as any,
chooseData: null as any,
addProjectDialog: {
title: '新增项目',
visible: false,
form: { name: '', remark: '' },
},
showEnvDialog: {
visible: false,
envs: [],
title: '',
addVisible: false,
envForm: {
name: '',
remark: '',
projectId: 0,
},
},
showMemDialog: {
visible: false,
chooseId: null,
chooseData: null,
query: {
pageSize: 8,
pageNum: 1,
projectId: null,
},
members: {
list: [],
total: null,
},
title: '',
addVisible: false,
memForm: {},
accounts: [],
},
});
onMounted(() => {
search();
});
const search = async () => {
let res = await projectApi.projects.request(state.query);
state.projects = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const showAddProjectDialog = (data: any) => {
if (data) {
state.addProjectDialog.form = data;
} else {
state.addProjectDialog.form = {} as any;
}
state.addProjectDialog.visible = true;
};
const cancelAddProject = () => {
state.addProjectDialog.visible = false;
state.addProjectDialog.form = {} as any;
};
const addProject = async () => {
const form = state.addProjectDialog.form as any;
notEmpty(form.name, '项目名不能为空');
notEmpty(form.remark, '项目描述不能为空');
await projectApi.saveProject.request(form);
ElMessage.success('保存成功');
search();
cancelAddProject();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const showMembers = async (project: any) => {
state.showMemDialog.query.projectId = project.id;
await setMemebers();
state.showMemDialog.title = `${project.name}的成员信息`;
state.showMemDialog.visible = true;
};
/**
* 选中成员
*/
const chooseMember = (item: any) => {
if (!item) {
return;
}
state.showMemDialog.chooseData = item;
state.showMemDialog.chooseId = item.id;
};
const deleteMember = async () => {
notNull(state.showMemDialog.chooseData, '请选选择成员');
await projectApi.deleteProjectMem.request(state.showMemDialog.chooseData);
ElMessage.success('移除成功');
// 重新赋值成员列表
setMemebers();
};
/**
* 设置成员列表信息
*/
const setMemebers = async () => {
const res = await projectApi.projectMems.request(state.showMemDialog.query);
state.showMemDialog.members.list = res.list;
state.showMemDialog.members.total = res.total;
};
const showEnv = async (project: any) => {
state.showEnvDialog.envs = await projectApi.projectEnvs.request({ projectId: project.id });
state.showEnvDialog.title = `${project.name}的环境信息`;
state.showEnvDialog.visible = true;
};
const showAddMemberDialog = () => {
state.showMemDialog.addVisible = true;
};
const addMember = async () => {
const memForm = state.showMemDialog.memForm as any;
memForm.projectId = state.chooseData.id;
notEmpty(memForm.accountId, '请先选择账号');
await projectApi.saveProjectMem.request(memForm);
ElMessage.success('保存成功');
setMemebers();
cancelAddMember();
};
const cancelAddMember = () => {
state.showMemDialog.memForm = {};
state.showMemDialog.addVisible = false;
state.showMemDialog.chooseData = null;
state.showMemDialog.chooseId = null;
};
const getAccount = (username: any) => {
accountApi.list.request({ username }).then((res) => {
state.showMemDialog.accounts = res.list;
});
};
const showAddEnvDialog = () => {
state.showEnvDialog.addVisible = true;
};
const addEnv = async () => {
const envForm = state.showEnvDialog.envForm;
envForm.projectId = state.chooseData.id;
await projectApi.saveProjectEnv.request(envForm);
ElMessage.success('保存成功');
state.showEnvDialog.envs = await projectApi.projectEnvs.request({ projectId: envForm.projectId });
cancelAddEnv();
};
const cancelAddEnv = () => {
state.showEnvDialog.envForm = {} as any;
state.showEnvDialog.addVisible = false;
};
const roleEditChange = (data: any) => {
ElMessage.success('修改成功!');
search();
};
return {
...toRefs(state),
search,
handlePageChange,
choose,
showAddProjectDialog,
addProject,
cancelAddProject,
showMembers,
setMemebers,
showEnv,
showAddMemberDialog,
addMember,
chooseMember,
deleteMember,
cancelAddMember,
showAddEnvDialog,
addEnv,
cancelAddEnv,
getAccount,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,15 @@
import Api from '@/common/Api';
export const projectApi = {
// 获取账号可访问的项目列表
accountProjects: Api.create("/accounts/projects", 'get'),
projects: Api.create("/projects", 'get'),
saveProject: Api.create("/projects", 'post'),
// 获取项目下的环境信息
projectEnvs: Api.create("/projects/{projectId}/envs", 'get'),
saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'),
// 获取项目下的成员信息
projectMems: Api.create("/projects/{projectId}/members", 'get'),
saveProjectMem: Api.create("/projects/{projectId}/members", 'post'),
deleteProjectMem: Api.create("/projects/{projectId}/members/{accountId}", 'delete'),
}

View File

@@ -0,0 +1,281 @@
<template>
<div>
<div class="toolbar">
<div style="float: left">
<el-row type="flex" justify="space-between">
<el-col :span="24">
<project-env-select @changeProjectEnv="changeProjectEnv" @clear="clearRedis">
<template #default>
<el-form-item label="redis" label-width="40px">
<el-select v-model="scanParam.id" placeholder="请选择redis" @change="changeRedis" @clear="clearRedis" clearable>
<el-option v-for="item in redisList" :key="item.id" :label="item.host" :value="item.id">
<span style="float: left">{{ item.host }}</span>
<span style="float: right; color: #8492a6; margin-left: 6px; font-size: 13px">{{
`库: [${item.db}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="key" label-width="40px">
<el-input
placeholder="支持*模糊key"
style="width: 180px"
v-model="scanParam.match"
size="mini"
@clear="clear()"
clearable
></el-input>
</el-form-item>
<el-form-item label-width="40px">
<el-input placeholder="count" style="width: 62px" v-model="scanParam.count" size="mini"></el-input>
</el-form-item>
<el-button @click="searchKey()" type="success" icon="el-icon-search" size="mini" plain></el-button>
<el-button @click="scan()" icon="el-icon-bottom" size="mini" plain>scan</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="save(false)" plain></el-button>
</template>
</project-env-select>
</el-col>
</el-row>
</div>
<div style="float: right">
<!-- <el-button @click="scan()" icon="el-icon-refresh" size="small" plain>刷新</el-button> -->
<span>keys: {{ dbsize }}</span>
</div>
</div>
<el-table v-loading="loading" :data="keys" border stripe :highlight-current-row="true" style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80"> </el-table-column>
<el-table-column prop="ttl" label="ttl(过期时间)" width="120">
<template #default="scope">
{{ ttlConveter(scope.row.ttl) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="el-icon-search" size="mini" plain>查看</el-button>
<el-button @click="del(scope.row.key)" type="danger" size="mini" icon="el-icon-delete" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: center; margin-top: 10px"></div>
<value-dialog v-model:visible="valueDialog.visible" :keyValue="valueDialog.value" />
</div>
</template>
<script lang="ts">
import ValueDialog from './ValueDialog.vue';
import { redisApi } from './api';
import { toRefs, reactive, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
import { isTrue, notNull } from '@/common/assert';
export default defineComponent({
name: 'DataOperation',
components: {
ValueDialog,
ProjectEnvSelect,
},
setup() {
const state = reactive({
loading: false,
cluster: 0,
redisList: [],
query: {
envId: 0,
},
// redis: {
// id: 0,
// info: '',
// conf: '',
// },
scanParam: {
id: null,
cluster: 0,
match: null,
count: 10,
cursor: 0,
prevCursor: null,
},
valueDialog: {
visible: false,
value: {},
},
keys: [],
dbsize: 0,
});
const searchRedis = async () => {
notNull(state.query.envId, '请先选择项目环境');
const res = await redisApi.redisList.request(state.query);
state.redisList = res.list;
};
const changeProjectEnv = (projectId: any, envId: any) => {
clearRedis();
if (envId != null) {
state.query.envId = envId;
searchRedis();
}
};
const changeRedis = (redisId: any) => {
resetScanParam();
state.keys = [];
state.dbsize = 0;
searchKey();
};
const scan = () => {
isTrue(state.scanParam.id != null, '请先选择redis');
isTrue(state.scanParam.count < 2001, 'count不能超过2000');
state.loading = true;
state.scanParam.cluster = state.cluster == 0 ? 0 : 1;
redisApi.scan.request(state.scanParam).then((res) => {
state.keys = res.keys;
state.dbsize = res.dbSize;
state.scanParam.cursor = res.cursor;
state.loading = false;
});
};
const searchKey = () => {
state.scanParam.cursor = 0;
scan();
};
const clearRedis = () => {
state.redisList = [];
state.scanParam.id = null;
resetScanParam();
state.keys = [];
state.dbsize = 0;
};
const clear = () => {
resetScanParam();
if (state.scanParam.id) {
scan();
}
};
const resetScanParam = () => {
state.scanParam.match = null;
state.scanParam.cursor = 0;
state.scanParam.count = 10;
};
const getValue = async (row: any) => {
let api: any;
switch (row.type) {
case 'string':
api = redisApi.getStringValue;
break;
case 'hash':
api = redisApi.getHashValue;
break;
case 'set':
api = redisApi.getSetValue;
break;
default:
api = redisApi.getStringValue;
break;
}
const id = state.cluster == 0 ? state.scanParam.id : state.cluster;
const res = await api.request({
cluster: state.cluster,
key: row.key,
id,
});
let timed = row.ttl == 18446744073709552000 ? 0 : row.ttl;
state.valueDialog.value = { id: state.scanParam.id, key: row.key, value: res, timed: timed, type: row.type };
state.valueDialog.visible = true;
};
// closeValueDialog() {
// this.valueDialog.visible = false
// this.valueDialog.value = {}
// }
const update = (key: string) => {};
const del = (key: string) => {
ElMessageBox.confirm(`此操作将删除对应的key , 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
let id = state.cluster == 0 ? state.scanParam.id : state.cluster;
redisApi.delKey
.request({
cluster: state.cluster,
key,
id,
})
.then((res) => {
ElMessage.success('删除成功!');
scan();
});
})
.catch((err) => {});
};
const ttlConveter = (ttl: any) => {
if (ttl == 18446744073709552000) {
return '永久';
}
if (!ttl) {
ttl = 0;
}
let second = parseInt(ttl); // 秒
let min = 0; // 分
let hour = 0; // 小时
let day = 0;
if (second > 60) {
min = parseInt(second / 60 + '');
second = second % 60;
if (min > 60) {
hour = parseInt(min / 60 + '');
min = min % 60;
if (hour > 24) {
day = parseInt(hour / 24 + '');
hour = hour % 24;
}
}
}
let result = '' + second + 's';
if (min > 0) {
result = '' + min + 'm:' + result;
}
if (hour > 0) {
result = '' + hour + 'h:' + result;
}
if (day > 0) {
result = '' + day + 'd:' + result;
}
return result;
};
return {
...toRefs(state),
changeProjectEnv,
changeRedis,
clearRedis,
searchKey,
scan,
clear,
getValue,
del,
ttlConveter,
};
},
});
</script>
<style>
</style>

View File

@@ -0,0 +1,200 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="true" width="35%" @close="close()">
<el-collapse>
<el-collapse-item title="Server(Redis服务器的一般信息)" name="server">
<div class="row">
<span class="title">redis_version(版本):</span>
<span class="value">{{ info.Server.redis_version }}</span>
</div>
<div class="row">
<span class="title">tcp_port(端口):</span>
<span class="value">{{ info.Server.tcp_port }}</span>
</div>
<div class="row">
<span class="title">redis_mode(模式):</span>
<span class="value">{{ info.Server.redis_mode }}</span>
</div>
<div class="row">
<span class="title">os(宿主操作系统):</span>
<span class="value">{{ info.Server.os }}</span>
</div>
<div class="row">
<span class="title">uptime_in_days(运行天数):</span>
<span class="value">{{ info.Server.uptime_in_days }}</span>
</div>
<div class="row">
<span class="title">executable(可执行文件路径):</span>
<span class="value">{{ info.Server.executable }}</span>
</div>
<div class="row">
<span class="title">config_file(配置文件路径):</span>
<span class="value">{{ info.Server.config_file }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Clients(客户端连接)" name="client">
<div class="row">
<span class="title">connected_clients(已连接客户端数):</span>
<span class="value">{{ info.Clients.connected_clients }}</span>
</div>
<div class="row">
<span class="title">blocked_clients(正在等待阻塞命令客户端数):</span>
<span class="value">{{ info.Clients.blocked_clients }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Keyspace(key信息)" name="keyspace">
<div class="row" v-for="(value, key) in info.Keyspace" :key="key">
<span class="title">{{ key }}: </span>
<span class="value">{{ value }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Stats(统计)" name="state">
<div class="row">
<span class="title">total_commands_processed(总处理命令数):</span>
<span class="value">{{ info.Stats.total_commands_processed }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
<div class="row">
<span class="title">total_net_input_bytes(网络入口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_input_bytes }}</span>
</div>
<div class="row">
<span class="title">total_net_output_bytes(网络出口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_output_bytes }}</span>
</div>
<div class="row">
<span class="title">expired_keys(过期key的总数量):</span>
<span class="value">{{ info.Stats.expired_keys }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Persistence(持久化)" name="persistence">
<div class="row">
<span class="title">aof_enabled(是否启用aof):</span>
<span class="value">{{ info.Persistence.aof_enabled }}</span>
</div>
<div class="row">
<span class="title">loading(是否正在载入持久化文件):</span>
<span class="value">{{ info.Persistence.loading }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Cluster(集群)" name="cluster">
<div class="row">
<span class="title">cluster_enabled(是否启用集群模式):</span>
<span class="value">{{ info.Cluster.cluster_enabled }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Memory(内存消耗相关信息)" name="memory">
<div class="row">
<span class="title">used_memory(分配内存总量):</span>
<span class="value">{{ info.Memory.used_memory_human }}</span>
</div>
<div class="row">
<span class="title">maxmemory(最大内存配置):</span>
<span class="value">{{ info.Memory.maxmemory }}</span>
</div>
<div class="row">
<span class="title">used_memory_rss(已分配的内存总量操作系统角度):</span>
<span class="value">{{ info.Memory.used_memory_rss_human }}</span>
</div>
<div class="row">
<span class="title">mem_fragmentation_ratio(used_memory_rss和used_memory 之间的比率):</span>
<span class="value">{{ info.Memory.mem_fragmentation_ratio }}</span>
</div>
<div class="row">
<span class="title">used_memory_peak(内存消耗峰值):</span>
<span class="value">{{ info.Memory.used_memory_peak_human }}</span>
</div>
<div class="row">
<span class="title">total_system_memory(主机总内存):</span>
<span class="value">{{ info.Memory.total_system_memory_human }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="CPU" name="cpu">
<div class="row">
<span class="title">used_cpu_sys(由Redis服务器消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user(由Redis服务器消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user }}</span>
</div>
<div class="row">
<span class="title">used_cpu_sys_children(由后台进程消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys_children }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user_children(由后台进程消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user_children }}</span>
</div>
</el-collapse-item>
</el-collapse>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
export default defineComponent({
name: 'Info',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
info: {
type: [Boolean, Object],
},
},
setup(props: any, { emit }) {
const state = reactive({
visible: false,
});
watch(
() => props.visible,
(val) => {
state.visible = val;
}
);
const close = () => {
emit('update:visible', false);
emit('close');
};
return {
...toRefs(state),
close,
};
},
});
</script>
<style>
.row .title {
font-size: 12px;
color: #8492a6;
margin-right: 6px;
}
.row .value {
font-size: 12px;
color: black;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="redisForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入host:port" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-input v-model.trim="form.db" placeholder="请输入库号"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { redisApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'RedisEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
redis: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const redisForm: any = ref(null);
const state = reactive({
visible: false,
projects: [],
envs: [],
form: {
id: null,
name: null,
host: null,
password: null,
project: null,
projectId: null,
envId: null,
env: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip:port',
trigger: ['change', 'blur'],
},
],
db: [
{
required: true,
message: '请输入库号',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
state.projects = newValue.projects;
if (newValue.redis) {
getEnvs(newValue.redis.projectId);
state.form = { ...newValue.redis };
} else {
state.envs = [];
state.form = { db: 0 } as any;
}
});
const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.project = p.name;
}
}
state.envs = [];
getEnvs(projectId);
};
const changeEnv = (envId: number) => {
for (let p of state.envs as any) {
if (p.id == envId) {
state.form.env = p.name;
}
}
};
const btnOk = async () => {
redisForm.value.validate((valid: boolean) => {
if (valid) {
redisApi.saveRedis.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
redisForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
redisForm,
changeProject,
changeEnv,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,369 @@
<template>
<div>
<div class="toolbar">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="editRedis(true)" plain>添加</el-button>
<el-button type="primary" icon="el-icon-edit" :disabled="currentId == null" size="mini" @click="editRedis(false)" plain>编辑</el-button>
<el-button type="danger" icon="el-icon-delete" :disabled="currentId == null" size="mini" @click="deleteRedis" plain>删除</el-button>
<div style="float: right">
<!-- <el-input placeholder="host" size="mini" style="width: 140px" v-model="query.host" @clear="search" plain clearable></el-input>
<el-select v-model="params.clusterId" size="mini" clearable placeholder="集群选择">
<el-option v-for="item in clusters" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select> -->
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable size="small">
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
<el-button class="ml5" @click="search" type="success" icon="el-icon-search" size="mini"></el-button>
</div>
</div>
<el-table :data="redisTable" stripe style="width: 100%" @current-change="choose">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="currentId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="project" label="项目" width></el-table-column>
<el-table-column prop="env" label="环境" width></el-table-column>
<el-table-column prop="host" label="host:port" width></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人"></el-table-column>
<el-table-column label="操作" width>
<template #default="scope">
<el-button type="primary" @click="info(scope.row)" icon="el-icon-tickets" size="mini" plain>info</el-button>
<!-- <el-button type="success" @click="manage(scope.row)" :ref="scope.row" size="mini" plain>数据管理</el-button> -->
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<info v-model:visible="infoDialog.visible" :title="infoDialog.title" :info="infoDialog.info"></info>
<redis-edit
@val-change="valChange"
:projects="projects"
:title="redisEditDialog.title"
v-model:visible="redisEditDialog.visible"
v-model:redis="redisEditDialog.data"
></redis-edit>
</div>
</template>
<script lang="ts">
import Info from './Info.vue';
import { redisApi } from './api';
import { toRefs, reactive, defineComponent, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { projectApi } from '../project/api.ts';
import RedisEdit from './RedisEdit.vue';
export default defineComponent({
name: 'RedisList',
components: {
Info,
RedisEdit,
},
setup() {
const state = reactive({
projects: [],
redisTable: [],
total: 0,
currentId: null,
currentData: null,
query: {
pageNum: 1,
pageSize: 10,
prjectId: null,
clusterId: null,
},
redisInfo: {
url: '',
},
clusters: [
{
id: 0,
name: '单机',
},
],
infoDialog: {
title: '',
visible: false,
info: {
Server: {},
Keyspace: {},
Clients: {},
CPU: {},
Memory: {},
},
},
redisEditDialog: {
visible: false,
data: null,
title: '新增redis',
},
});
onMounted(async () => {
search();
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
});
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
// connect() {
// Req.post('/open/redis/connect', this.form, res => {
// this.redisInfo = res
// })
// }
const deleteRedis = async () => {
try {
await ElMessageBox.confirm(`确定删除该redis?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await redisApi.delRedis.request({ id: state.currentId });
ElMessage.success('删除成功');
state.currentData = null;
state.currentId = null;
search();
} catch (err) {}
};
const info = (redis: any) => {
redisApi.redisInfo.request({ id: redis.id }).then((res: any) => {
state.infoDialog.info = res;
state.infoDialog.title = `'${redis.host}' info`;
state.infoDialog.visible = true;
});
};
const search = async () => {
const res = await redisApi.redisList.request(state.query);
state.redisTable = res.list;
state.total = res.total;
};
const editRedis = (isAdd = false) => {
if (isAdd) {
state.redisEditDialog.data = null;
state.redisEditDialog.title = '新增redis';
} else {
state.redisEditDialog.data = state.currentData;
state.redisEditDialog.title = '修改redis';
}
state.redisEditDialog.visible = true;
};
const valChange = () => {
search();
};
return {
...toRefs(state),
search,
handlePageChange,
choose,
info,
deleteRedis,
editRedis,
valChange,
};
},
});
// @Component({
// name: 'RedisList',
// components: {
// Info,
// DynamicFormDialog
// }
// })
// export default class RedisList extends Vue {
// validatePort = (rule: any, value: any, callback: any) => {
// if (value > 65535 || value < 1) {
// callback(new Error('端口号错误'))
// }
// callback()
// }
// redisTable = []
// permission = redisPermission
// keyPermission = redisKeyPermission
// currentId = null
// currentData: any = null
// params = {
// host: null,
// clusterId: null
// }
// redisInfo = {
// url: ''
// }
// clusters = [
// {
// id: 0,
// name: '单机'
// }
// ]
// infoDialog = {
// title: '',
// visible: false,
// info: {
// Server: {},
// Keyspace: {},
// Clients: {},
// CPU: {},
// Memory: {}
// }
// }
// formDialog = {
// visible: false,
// title: '',
// formInfo: {
// createApi: redisApi.save,
// updateApi: redisApi.update,
// formRows: [
// [
// {
// type: 'input',
// label: '主机:',
// name: 'host',
// placeholder: '请输入节点ip',
// rules: [
// {
// required: true,
// message: '请输入节点ip',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '端口号:',
// name: 'port',
// placeholder: '请输入节点端口号',
// inputType: 'number',
// rules: [
// {
// required: true,
// message: '请输入节点端口号',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '密码:',
// name: 'pwd',
// placeholder: '请输入节点密码',
// inputType: 'password'
// }
// ],
// [
// {
// type: 'input',
// label: '描述:',
// name: 'description',
// placeholder: '请输入节点描述',
// inputType: 'textarea'
// }
// ]
// ]
// },
// formData: { port: 6379 }
// }
// mounted() {
// this.search()
// }
// choose(item: any) {
// if (!item) {
// return
// }
// this.currentId = item.id
// this.currentData = item
// }
// // connect() {
// // Req.post('/open/redis/connect', this.form, res => {
// // this.redisInfo = res
// // })
// // }
// async deleteNode() {
// await redisApi.del.request({ id: this.currentId })
// this.$message.success('删除成功')
// this.search()
// }
// manage(row: any) {
// this.$router.push(`/redis_operation/${row.clusterId}/${row.id}`)
// }
// info(redis: any) {
// redisApi.info.request({ id: redis.id }).then(res => {
// this.infoDialog.info = res
// this.infoDialog.title = `'${redis.host}' info`
// this.infoDialog.visible = true
// })
// }
// search() {
// redisApi.list.request(this.params).then(res => {
// this.redisTable = res
// })
// }
// openFormDialog(redis: any) {
// let dialogTitle
// if (redis) {
// this.formDialog.formData = this.currentData
// dialogTitle = '编辑redis节点'
// } else {
// this.formDialog.formData = { port: 6379 }
// dialogTitle = '添加redis节点'
// }
// this.formDialog.title = dialogTitle
// this.formDialog.visible = true
// }
// submitSuccess() {
// this.currentId = null
// this.currentData = null
// this.search()
// }
// }
</script>
<style>
</style>

View File

@@ -0,0 +1,76 @@
<template>
<el-dialog :title="keyValue.key" v-model="visible" :before-close="cancel" :show-close="false" width="750px">
<el-form>
<el-form-item>
<el-input v-model="keyValue.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="saveValue" type="primary" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { isTrue } from '@/common/assert';
export default defineComponent({
name: 'ValueDialog',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
keyValue: {
type: [String, Object],
},
},
setup(props: any, { emit }) {
const state = reactive({
visible: false,
keyValue: {} as any,
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
watch(
() => props.visible,
(val) => {
state.visible = val;
}
);
watch(
() => props.keyValue,
(val) => {
state.keyValue = val;
if (state.keyValue.type != 'string') {
state.keyValue.value = JSON.stringify(val.value, undefined, 2)
}
// state.keyValue.value = JSON.stringify(val.value, undefined, 2)
}
);
const saveValue = async () => {
isTrue(state.keyValue.type == 'string', "暂不支持除string外其他类型修改")
await redisApi.saveStringValue.request(state.keyValue);
ElMessage.success('保存成功');
cancel();
};
return {
...toRefs(state),
saveValue,
cancel,
};
},
});
</script>

View File

@@ -0,0 +1,17 @@
import Api from '@/common/Api';
export const redisApi = {
redisList : Api.create("/redis", 'get'),
redisInfo: Api.create("/redis/{id}/info", 'get'),
saveRedis: Api.create("/redis", 'post'),
delRedis: Api.create("/redis/{id}", 'delete'),
// 获取权限列表
scan: Api.create("/redis/{id}/scan/{cursor}/{count}", 'get'),
getStringValue: Api.create("/redis/{id}/string-value", 'get'),
saveStringValue: Api.create("/redis/{id}/string-value", 'post'),
getHashValue: Api.create("/redis/{id}/hash-value", 'get'),
getSetValue: Api.create("/redis/{id}/set-value", 'get'),
saveHashValue: Api.create("/redis/{id}/hash-value", 'post'),
del: Api.create("/redis/{id}/scan/{cursor}/{count}", 'delete'),
delKey: Api.create("/redis/{id}/key", 'delete'),
}

View File

@@ -0,0 +1 @@
export { default } from './RedisList.vue';

View File

@@ -3,14 +3,14 @@
<el-dialog :title="title" v-model="visible" :show-close="false" width="35%">
<el-form :model="form" ref="accountForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="username" label="用户名:" required>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入用户名" auto-complete="off"></el-input>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<!-- <el-form-item prop="password" label="密码:" required>
<el-input type="password" v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item v-if="!edit" label="确认密码:" required>
<el-input type="password" v-model.trim="form.repassword" placeholder="请输入确认密码" autocomplete="new-password"></el-input>
</el-form-item>
</el-form-item> -->
</el-form>
<template #footer>
@@ -61,13 +61,13 @@ export default defineComponent({
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
// password: [
// {
// required: true,
// message: '请输入密码',
// trigger: ['change', 'blur'],
// },
// ],
},
});

View File

@@ -19,6 +19,7 @@
>
<div style="float: right">
<el-input
class="mr2"
placeholder="请输入账号名"
size="small"
style="width: 140px"
@@ -45,8 +46,13 @@
<el-tag v-if="scope.row.status == -1" type="danger" size="mini">禁用</el-tag>
</template>
</el-table-column>
<el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.lastLoginTime) }}
</template>
</el-table-column>
<!-- <el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column> -->
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
<el-table-column min-width="160" prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
@@ -58,8 +64,8 @@
{{ $filters.dateFormat(scope.row.updateTime) }}
</template>
</el-table-column> -->
<el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间"></el-table-column>
<el-table-column min-width="120" prop="remark" label="备注" show-overflow-tooltip></el-table-column>
<!-- <el-table-column min-width="120" prop="remark" label="备注" show-overflow-tooltip></el-table-column> -->
<el-table-column label="查看更多" min-width="150">
<template #default="scope">
<el-link @click.prevent="showRoles(scope.row)" type="success">角色</el-link>
@@ -70,14 +76,21 @@
<el-table-column label="操作" min-width="200px">
<template #default="scope">
<el-button v-auth="'account:changeStatus'" v-if="scope.row.status == 1" type="danger" icom="el-icon-tickets" size="mini" plain
<el-button
v-auth="'account:changeStatus'"
@click="changeStatus(scope.row)"
v-if="scope.row.status == 1"
type="danger"
icom="el-icon-tickets"
size="mini"
plain
>禁用</el-button
>
<el-button
v-auth="'account:changeStatus'"
v-if="scope.row.status == -1"
type="success"
@click="serviceManager(scope.row)"
@click="changeStatus(scope.row)"
size="mini"
plain
>启用</el-button
@@ -136,7 +149,7 @@ import RoleEdit from './RoleEdit.vue';
import AccountEdit from './AccountEdit.vue';
import enums from '../enums';
import { accountApi } from '../api';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
export default defineComponent({
name: 'AccountList',
components: {
@@ -223,13 +236,13 @@ export default defineComponent({
const changeStatus = async (row: any) => {
let id = row.id;
let status = row.status ? 1 : -1;
// await accountApi.changeStatus.request({
// id,
// status,
// });
// ElMessage.success('操作成功');
// search();
let status = row.status == -1 ? 1 : -1;
await accountApi.changeStatus.request({
id,
status,
});
ElMessage.success('操作成功');
search();
};
const handlePageChange = (curPage: number) => {
@@ -267,12 +280,17 @@ export default defineComponent({
const deleteAccount = async () => {
try {
await ElMessageBox.confirm(`确定删除该账号?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await accountApi.del.request({ id: state.chooseId });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (error) {
ElMessage.error('刪除失败');
}
} catch (err) {}
};
return {

View File

@@ -13,8 +13,9 @@
</div>
</div>
<el-table :data="allRole" border ref="roleTable" @select="select" style="width: 100%">
<el-table-column type="selection" width="40"></el-table-column>
<el-table-column :selectable="selectable" type="selection" width="40"></el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色code"></el-table-column>
<el-table-column prop="remark" label="角色描述">
<template #default="scope">
{{ scope.row.remark ? scope.row.remark : '暂无描述' }}
@@ -92,6 +93,11 @@ export default defineComponent({
search();
};
const selectable = (row: any) => {
// 角色code不以COMMON开头才可勾选
return row.code.indexOf('COMMON') != 0;
};
const select = (val: any, row: any) => {
let roles = state.roles;
// 如果账号的角色id存在则为取消该角色(删除角色id列表中的该记录id),否则为新增角色
@@ -164,6 +170,7 @@ export default defineComponent({
roleTable,
search,
handlePageChange,
selectable,
select,
btnOk,
cancel,

View File

@@ -25,7 +25,7 @@ export const accountApi = {
save: Api.create("/sys/accounts", 'post'),
update: Api.create("/sys/accounts/{id}", 'put'),
del: Api.create("/sys/accounts/{id}", 'delete'),
changeStatus: Api.create("/sys/accounts/{id}/{status}", 'put'),
changeStatus: Api.create("/sys/accounts/change-status/{id}/{status}", 'put'),
roleIds: Api.create("/sys/accounts/{id}/roleIds", 'get'),
roles: Api.create("/sys/accounts/{id}/roles", 'get'),
resources: Api.create("/sys/accounts/{id}/resources", 'get'),

View File

@@ -13,7 +13,7 @@
</el-form-item>
<el-form-item prop="code" label="path|code">
<el-input v-model.trim="form.code" placeholder="菜单为路由path"></el-input>
<el-input v-model.trim="form.code" placeholder="菜单不带/自动拼接父路径"></el-input>
</el-form-item>
<el-form-item label="序号" prop="weight" required>

View File

@@ -5,6 +5,9 @@
<el-form-item label="角色名称:" required>
<el-input v-model="form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="角色code:" required>
<el-input :disabled="form.id" v-model="form.code" placeholder="COMMON开头则为所有账号共有角色" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="角色描述:">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入角色描述"></el-input>
</el-form-item>

View File

@@ -44,6 +44,7 @@
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色code"></el-table-column>
<el-table-column prop="remark" label="描述" min-width="180px" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">

View File

@@ -1,7 +1,7 @@
package initialize
import (
"mayfly-go/base/global"
"mayfly-go/base/config"
"mayfly-go/base/middleware"
"mayfly-go/mock-server/routers"
@@ -10,7 +10,7 @@ import (
func InitRouter() *gin.Engine {
// server配置
serverConfig := global.Config.Server
serverConfig := config.Conf.Server
gin.SetMode(serverConfig.Model)
var router = gin.New()

Binary file not shown.

View File

@@ -1,5 +1,5 @@
app:
name: devops
name: mayfly-go
version: 1.0.0
server:
@@ -7,6 +7,10 @@ server:
model: release
port: 8888
cors: true
tls:
enable: false
key-file: ./default.key
cert-file: ./default.pem
# 静态资源
static:
- relative-path: /assets
@@ -18,6 +22,11 @@ server:
- relative-path: /favicon.ico
filepath: ./static/favicon.ico
jwt:
key: mykey
# 过期时间单位分钟
expire-time: 1440
redis:
host: 127.0.0.1
port: 6379
@@ -27,4 +36,11 @@ mysql:
username: root
password: 111049
db-name: mayfly-job
config: charset=utf8&loc=Local&parseTime=true
config: charset=utf8&loc=Local&parseTime=true
log:
# 日志等级, trace, debug, info, warn, error, fatal
level: info
# file:
# path: ./
# name: mayfly.log

27
server/default.key Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEA0CsawvEZl42Vf+0BlTuZ3Dp10yW8Oty1tjimxUj3s0WPeKil
6+TehnQELS8vGJfek+yT99nyrt+bkRmg1kxZ57FtQFEuthG4OQZoaMDUz6Ab+8P1
PQ9VH0XimnnYabxztJiQjl8HdJt6N4WP35kGlcul7qQ+Qc7iwjhSadfAhXVycqVI
cGQyHiPPfbmYRjueAIC4czmMUxwFKCwjepGYkwzWuGkpMD0hg/SIXpFJE2dcqYPR
2nCah1gxZZG00lHU1X2pehNmmgeHRkB5S7mrsCdyyV/33SAYk6T6PT7dOqY54bfn
h3C0k+T7IzvKTXKG76eG63STmxVa6luVoKMvxwIDAQABAoIBAQCI2Y2CUpYMd9us
edbskH4ZtaT35nrUB3y+Cog4cjvE8xnarKRHa/KOWX7VZYuEk3KTtJeh/Pn51K6k
uUBvIUqJcq7r9XLL5uJBOuEw3HQK+qrq3GxAc+/12y+Zdji7alR2iUWfEwIHup6i
GX/38tXNbE/tjrQO9z9Dh1tGkbvS/66tPn/T/oMxsRvZB6mCjB7yuOlEIwYTomYB
pUFemELt8T5RtfxRa8T1VoITbfuj7zvecqlThW0H8UizsFxvrOCUaga7jtsJOCHo
bcW5WvWwazoOfQ2BGpksKkBDf1N6pj85e4kOoYcVG9UN03ZwDvAGfQPWUlHB4YzW
PybMwIQBAoGBAPfuOQ+ukVmkiEKj6wCBe3Z0pYeNqBGec9aj62bKFh79BCE3ZopS
7JtGs8VfBKkBAaOy+MDuvJ2fvRNRtHT4BYe1U6ZRsmVFqHScACOaO/7TR0tz0ihL
0QLCkbSwsXExG6bYbwP4jMHkhHArT7Hy8WXvup8PffjSiEs1A1uGvYSBAoGBANbx
lHo+39nsc1OO8TUAWZChIQUib2hFIwzQYngSzINdfXQaGFT/omOsudAtfdjvp+qO
Tr7WpwgFEFveDFsdJfZ2Kc2x9a3ty7IYIWaAjK2ghkAKz3Tt4gClreB6qG2SBycP
4C2ImbY6hMaFHz3ENtTEzzTMdD1ByxQVMvoem3BHAoGBAJdaTmtMXl8jGivUdXnx
kbVWsFZ4G8nluUGm/+XYKHjybLr6XxbCWL7SApzSzL1/Z8jPURw2od53za0li8x8
PKQEBfTamtVIGPZW5Z7WYRnHURa2tezzm7zbmqd71lcLa54HMn5yFTuojVEMn7I6
ZTOdjYfcpUJpA9slmc8eCkQBAoGBAJEIbxRRaoBEQMkH8Y++zbB+WKZ7RssHo4/Y
6Ch3HtIg+i6mEPcBitRQzww+NeV0SExHe7Dfa9NIf3JNkO7F60CzGJ/3zXtvsftY
tujQIpxhbVS3NqaCgPXI1VtbyFwupW7hEnYG7xj7wW2mk578z7afmeTZdDGFPH8v
krccgeuvAoGBAPAwiqbZlXNx+ueI1B3T8VpXnG0ozKxG+l5B71kssZWa7xcv9yRd
c15l2PSXNtnoT/mBID7+dqQOmfYxsDHAkUdd/BrxhXtdi9FR3AfHSEQz+VKsogAD
uLyRd7jWTYqqGa2UToF/CBV+c6QyMB+6pzFNk5DmUEm4Gd6jcHDITYeI
-----END RSA PRIVATE KEY-----

21
server/default.pem Normal file
View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDejCCAmICCQDQU4ZRt2G46TANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJ6
aDEPMA0GA1UECAwGbWF5Zmx5MQswCQYDVQQHDAJ4bTEPMA0GA1UECgwGbWF5Zmx5
MQ8wDQYDVQQLDAZtYXlmbHkxDzANBgNVBAMMBm1heWZseTEfMB0GCSqGSIb3DQEJ
ARYQOTU0NTM3NDczQHFxLmNvbTAeFw0yMTA2MjQwMzI2MzBaFw0zMTA2MjIwMzI2
MzBaMH8xCzAJBgNVBAYTAnpoMQ8wDQYDVQQIDAZtYXlmbHkxCzAJBgNVBAcMAnht
MQ8wDQYDVQQKDAZtYXlmbHkxDzANBgNVBAsMBm1heWZseTEPMA0GA1UEAwwGbWF5
Zmx5MR8wHQYJKoZIhvcNAQkBFhA5NTQ1Mzc0NzNAcXEuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CsawvEZl42Vf+0BlTuZ3Dp10yW8Oty1tjim
xUj3s0WPeKil6+TehnQELS8vGJfek+yT99nyrt+bkRmg1kxZ57FtQFEuthG4OQZo
aMDUz6Ab+8P1PQ9VH0XimnnYabxztJiQjl8HdJt6N4WP35kGlcul7qQ+Qc7iwjhS
adfAhXVycqVIcGQyHiPPfbmYRjueAIC4czmMUxwFKCwjepGYkwzWuGkpMD0hg/SI
XpFJE2dcqYPR2nCah1gxZZG00lHU1X2pehNmmgeHRkB5S7mrsCdyyV/33SAYk6T6
PT7dOqY54bfnh3C0k+T7IzvKTXKG76eG63STmxVa6luVoKMvxwIDAQABMA0GCSqG
SIb3DQEBCwUAA4IBAQB/e8EO2XEtkYBxebR1w6i50vaegLsxQJR3l5qm7rsHu3Cr
smJXGsc56axKCAqJ4XvSI65BT51FghAoGn62QNyiQgc0YoS99nwCCGFtnhZ2lmSe
pfhUHegN/Qo4I8FemEMD+o9kGeAzwrnaIVIT/cNOEQgm+RzrgHHJh5QBn2XgJalU
NeFTWaimyefwSezSa/vPbyMoAl9HkT6kdvnms/yOth4AOle6+5pM2StWjmMi4yx4
16y3NvLTku6nAUazaHTOOu/MCqLWL2/qYTk3r7OCop2jr9Rp+HLbg5AfKLUIVXjG
/1fnXJIuD+2u9qgDLN5PZNgz4MlU86ugtmYPFkVt
-----END CERTIFICATE-----

View File

@@ -6,6 +6,7 @@ import (
"mayfly-go/base/ctx"
"mayfly-go/base/ginx"
"mayfly-go/base/model"
"mayfly-go/base/utils"
"mayfly-go/server/devops/apis/form"
"mayfly-go/server/devops/apis/vo"
"mayfly-go/server/devops/application"
@@ -17,45 +18,73 @@ import (
)
type Db struct {
DbApp application.IDb
DbApp application.Db
}
// @router /api/dbs [get]
func (d *Db) Dbs(rc *ctx.ReqCtx) {
m := new(entity.Db)
g := rc.GinCtx
m := &entity.Db{EnvId: uint64(ginx.QueryInt(g, "envId", 0)),
ProjectId: uint64(ginx.QueryInt(g, "projectId", 0)),
Database: g.Query("database"),
}
ginx.BindQuery(g, m)
rc.ResData = d.DbApp.GetPageList(m, ginx.GetPageParam(rc.GinCtx), new([]vo.SelectDataDbVO))
}
// @router /api/db/:dbId/select [get]
func (d *Db) SelectData(rc *ctx.ReqCtx) {
func (d *Db) Save(rc *ctx.ReqCtx) {
form := &form.DbForm{}
ginx.BindJsonAndValid(rc.GinCtx, form)
rc.ReqParam = form
db := new(entity.Db)
utils.Copy(db, form)
db.SetBaseInfo(rc.LoginAccount)
d.DbApp.Save(db)
}
func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
d.DbApp.Delete(uint64(ginx.PathParamInt(rc.GinCtx, "id")))
}
// @router /api/db/:dbId/exec-sql [get]
func (d *Db) ExecSql(rc *ctx.ReqCtx) {
g := rc.GinCtx
// 去除前后空格及换行符
selectSql := strings.TrimFunc(g.Query("selectSql"), func(r rune) bool {
sql := strings.TrimFunc(g.Query("sql"), func(r rune) bool {
s := string(r)
return s == " " || s == "\n"
})
rc.ReqParam = selectSql
rc.ReqParam = sql
biz.NotEmpty(selectSql, "selectSql不能为空")
res, err := d.DbApp.GetDbInstance(GetDbId(g)).SelectData(selectSql)
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("查询失败: %s", err.Error())))
}
rc.ResData = res
}
// @router /api/db/:dbId/exec-sql [post]
func (d *Db) ExecSql(g *gin.Context) {
rc := ctx.NewReqCtxWithGin(g).WithLog(ctx.NewLogInfo("sql执行"))
rc.Handle(func(rc *ctx.ReqCtx) {
selectSql := g.Query("sql")
biz.NotEmpty(selectSql, "sql不能为空")
num, err := d.DbApp.GetDbInstance(GetDbId(rc.GinCtx)).Exec(selectSql)
biz.NotEmpty(sql, "sql不能为空")
if strings.HasPrefix(sql, "SELECT") || strings.HasPrefix(sql, "select") {
colNames, res, err := d.DbApp.GetDbInstance(GetDbId(g)).SelectData(sql)
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("查询失败: %s", err.Error())))
}
colAndRes := make(map[string]interface{})
colAndRes["colNames"] = colNames
colAndRes["res"] = res
rc.ResData = colAndRes
} else {
rowsAffected, err := d.DbApp.GetDbInstance(GetDbId(g)).Exec(sql)
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("执行失败: %s", err.Error())))
}
rc.ResData = num
})
res := make([]map[string]string, 0)
resData := make(map[string]string)
resData["影响条数"] = fmt.Sprintf("%d", rowsAffected)
res = append(res, resData)
colAndRes := make(map[string]interface{})
colAndRes["colNames"] = []string{"影响条数"}
colAndRes["res"] = res
rc.ResData = colAndRes
}
}
// @router /api/db/:dbId/t-metadata [get]
@@ -74,21 +103,32 @@ func (d *Db) ColumnMA(rc *ctx.ReqCtx) {
// @router /api/db/:dbId/hint-tables [get]
func (d *Db) HintTables(rc *ctx.ReqCtx) {
dbi := d.DbApp.GetDbInstance(GetDbId(rc.GinCtx))
// 获取所有表
tables := dbi.GetTableMetedatas()
res := make(map[string][]string)
tableNames := make([]string, 0)
for _, v := range tables {
tableName := v["tableName"]
columnMds := dbi.GetColumnMetadatas(tableName)
columnNames := make([]string, len(columnMds))
for i, v := range columnMds {
comment := v["columnComment"]
if comment != "" {
columnNames[i] = v["columnName"] + " [" + comment + "]"
} else {
columnNames[i] = v["columnName"]
}
tableNames = append(tableNames, v["tableName"])
}
// 获取所有表下的所有列信息
columnMds := dbi.GetColumnMetadatas(tableNames...)
// key = 表名value = 列名数组
res := make(map[string][]string)
for _, v := range columnMds {
tName := v["tableName"]
if res[tName] == nil {
res[tName] = make([]string, 0)
}
res[tableName] = columnNames
columnName := fmt.Sprintf("%s [%s]", v["columnName"], v["columnType"])
comment := v["columnComment"]
// 如果字段备注不为空,则加上备注信息
if comment != "" {
columnName = fmt.Sprintf("%s[%s]", columnName, comment)
}
res[tName] = append(res[tName], columnName)
}
rc.ResData = res
}

View File

@@ -0,0 +1,16 @@
package form
type DbForm struct {
Id uint64
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 `binding:"required" json:"password"`
Database string `binding:"required" json:"database"`
ProjectId uint64 `binding:"required" json:"projectId"`
Project string `json:"project"`
Env string `json:"env"`
EnvId uint64 `binding:"required" json:"envId"`
}

View File

@@ -2,45 +2,46 @@ package form
type MachineForm struct {
Id uint64 `json:"id"`
Name string `json:"name" valid:"Required"`
Name string `json:"name" binding:"required"`
// IP地址
Ip string `json:"ip" valid:"Required"`
Ip string `json:"ip" binding:"required"`
// 用户名
Username string `json:"username" valid:"Required"`
Password string `json:"password" valid:"Required"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
// 端口号
Port int `json:"port" valid:"Required"`
Port int `json:"port" binding:"required"`
}
type MachineRunForm struct {
MachineId int64 `valid:"Required"`
Cmd string `valid:"Required"`
MachineId int64 `binding:"required"`
Cmd string `binding:"required"`
}
type MachineFileForm struct {
Id uint64
Name string `valid:"Required"`
MachineId uint64 `valid:"Required"`
Type int `valid:"Required"`
Path string `valid:"Required"`
Name string `binding:"required"`
MachineId uint64 `binding:"required"`
Type int `binding:"required"`
Path string `binding:"required"`
}
type MachineScriptForm struct {
Id uint64
Name string `valid:"Required"`
MachineId uint64 `valid:"Required"`
Type int `valid:"Required"`
Description string `valid:"Required"`
Script string `valid:"Required"`
Name string `binding:"required"`
MachineId uint64 `binding:"required"`
Type int `binding:"required"`
Description string `binding:"required"`
Params string
Script string `binding:"required"`
}
type DbSqlSaveForm struct {
Sql string `valid:"Required"`
Type int `valid:"Required"`
Sql string `binding:"required"`
Type int `binding:"required"`
}
type MachineFileUpdateForm struct {
Content string `valid:"Required"`
Id uint64 `valid:"Required"`
Path string `valid:"Required"`
Content string `binding:"required"`
Id uint64 `binding:"required"`
Path string `binding:"required"`
}

View File

@@ -0,0 +1,18 @@
package form
type Redis struct {
Id uint64
Host string `binding:"required" json:"host"`
Password string `json:"password"`
Db int `json:"db"`
ProjectId uint64 `binding:"required" json:"projectId"`
Project string `json:"project"`
Env string `json:"env"`
EnvId uint64 `binding:"required" json:"envId"`
}
type KeyValue struct {
Key string `binding:"required" json:"key"`
Value interface{} `binding:"required" json:"value"`
Timed uint64
}

View File

@@ -26,7 +26,7 @@ var WsUpgrader = websocket.Upgrader{
}
type Machine struct {
MachineApp application.IMachine
MachineApp application.Machine
}
func (m *Machine) Machines(rc *ctx.ReqCtx) {
@@ -45,6 +45,12 @@ func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
m.MachineApp.Save(entity)
}
func (m *Machine) DeleteMachine(rc *ctx.ReqCtx) {
id := uint64(ginx.PathParamInt(rc.GinCtx, "id"))
rc.ReqParam = id
m.MachineApp.Delete(id)
}
// top命令信息
func (m *Machine) Top(rc *ctx.ReqCtx) {
rc.ResData = m.MachineApp.GetCli(GetMachineId(rc.GinCtx)).GetTop()

View File

@@ -19,8 +19,8 @@ import (
)
type MachineFile struct {
MachineFileApp application.IMachineFile
MachineApp application.IMachine
MachineFileApp application.MachineFile
MachineApp application.Machine
}
const (

View File

@@ -16,8 +16,8 @@ import (
)
type MachineScript struct {
MachineScriptApp application.IMachineScript
MachineApp application.IMachine
MachineScriptApp application.MachineScript
MachineApp application.Machine
}
func (m *MachineScript) MachineScripts(rc *ctx.ReqCtx) {
@@ -57,8 +57,12 @@ func (m *MachineScript) RunMachineScript(rc *ctx.ReqCtx) {
biz.NotNil(ms, "该脚本不存在")
biz.IsTrue(ms.MachineId == application.Common_Script_Machine_Id || ms.MachineId == machineId, "该脚本不属于该机器")
vars := g.QueryMap("params")
res, err := m.MachineApp.GetCli(machineId).Run(utils.TemplateParse(ms.Script, vars))
script := ms.Script
// 如果有脚本参数,则用脚本参数替换脚本中的模板占位符参数
if params := g.Query("params"); params != "" {
script = utils.TemplateParse(ms.Script, utils.Json2Map(params))
}
res, err := m.MachineApp.GetCli(machineId).Run(script)
// 记录请求参数
rc.ReqParam = fmt.Sprintf("[machineId: %d, scriptId: %d, name: %s]", machineId, scriptId, ms.Name)
if err != nil {

View File

@@ -0,0 +1,100 @@
package apis
import (
"fmt"
"mayfly-go/base/biz"
"mayfly-go/base/ctx"
"mayfly-go/base/ginx"
"mayfly-go/server/devops/apis/vo"
"mayfly-go/server/devops/application"
"mayfly-go/server/devops/domain/entity"
sys_applicaiton "mayfly-go/server/sys/application"
sys_entity "mayfly-go/server/sys/domain/entity"
)
type Project struct {
ProjectApp application.Project
AccountApp sys_applicaiton.Account
}
// 获取当前登录用户可以访问的项目列表
func (p *Project) GetProjectsByLoginAccount(rc *ctx.ReqCtx) {
// 获取登录用户拥有的项目ids
projectMembers := &[]entity.ProjectMember{}
p.ProjectApp.ListMember(&entity.ProjectMember{AccountId: rc.LoginAccount.Id}, projectMembers)
var pids []uint64
for _, pm := range *projectMembers {
pids = append(pids, pm.ProjectId)
}
// 获取项目信息
projects := &vo.AccountProjects{}
p.ProjectApp.ListProjectByIds(pids, projects)
rc.ResData = projects
}
func (p *Project) GetProjects(rc *ctx.ReqCtx) {
condition := &entity.Project{}
ginx.BindQuery(rc.GinCtx, condition)
// condition.Name = rc.GinCtx.Query("name")
rc.ResData = p.ProjectApp.GetPageList(condition, ginx.GetPageParam(rc.GinCtx), new([]entity.Project))
}
func (p *Project) SaveProject(rc *ctx.ReqCtx) {
project := &entity.Project{}
ginx.BindJsonAndValid(rc.GinCtx, project)
rc.ReqParam = project
project.SetBaseInfo(rc.LoginAccount)
p.ProjectApp.SaveProject(project)
}
// 获取项目下的环境信息
func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) {
projectEnvs := &[]entity.ProjectEnv{}
p.ProjectApp.ListEnvByProjectId(uint64(ginx.PathParamInt(rc.GinCtx, "projectId")), projectEnvs)
rc.ResData = projectEnvs
}
//保存项目下的环境信息
func (p *Project) SaveProjectEnvs(rc *ctx.ReqCtx) {
projectEnv := &entity.ProjectEnv{}
ginx.BindJsonAndValid(rc.GinCtx, projectEnv)
rc.ReqParam = projectEnv
projectEnv.SetBaseInfo(rc.LoginAccount)
p.ProjectApp.SaveProjectEnv(projectEnv)
}
// 获取项目下的成员信息
func (p *Project) GetProjectMembers(rc *ctx.ReqCtx) {
projectMems := &[]entity.ProjectMember{}
rc.ResData = p.ProjectApp.GetMemberPage(&entity.ProjectMember{ProjectId: uint64(ginx.PathParamInt(rc.GinCtx, "projectId"))},
ginx.GetPageParam(rc.GinCtx), projectMems)
}
//保存项目的成员信息
func (p *Project) SaveProjectMember(rc *ctx.ReqCtx) {
projectMem := &entity.ProjectMember{}
ginx.BindJsonAndValid(rc.GinCtx, projectMem)
rc.ReqParam = projectMem
// 校验账号并赋值username
account := &sys_entity.Account{}
account.Id = projectMem.AccountId
biz.ErrIsNil(p.AccountApp.GetAccount(account, "Id", "Username"), "账号不存在")
projectMem.Username = account.Username
projectMem.SetBaseInfo(rc.LoginAccount)
p.ProjectApp.SaveProjectMember(projectMem)
}
//删除项目成员
func (p *Project) DelProjectMember(rc *ctx.ReqCtx) {
g := rc.GinCtx
pid := ginx.PathParamInt(g, "projectId")
aid := ginx.PathParamInt(g, "accountId")
rc.ReqParam = fmt.Sprintf("projectId: %d, accountId: %d", pid, aid)
p.ProjectApp.DeleteMember(uint64(pid), uint64(aid))
}

203
server/devops/apis/redis.go Normal file
View File

@@ -0,0 +1,203 @@
package apis
import (
"mayfly-go/base/biz"
"mayfly-go/base/ctx"
"mayfly-go/base/ginx"
"mayfly-go/base/utils"
"mayfly-go/server/devops/apis/form"
"mayfly-go/server/devops/apis/vo"
"mayfly-go/server/devops/application"
"mayfly-go/server/devops/domain/entity"
"strconv"
"strings"
"time"
)
type Redis struct {
RedisApp application.Redis
}
func (r *Redis) RedisList(rc *ctx.ReqCtx) {
g := rc.GinCtx
m := &entity.Redis{EnvId: uint64(ginx.QueryInt(g, "envId", 0)),
ProjectId: uint64(ginx.QueryInt(g, "projectId", 0)),
}
ginx.BindQuery(g, m)
rc.ResData = r.RedisApp.GetPageList(m, ginx.GetPageParam(rc.GinCtx), new([]vo.Redis))
}
func (r *Redis) Save(rc *ctx.ReqCtx) {
form := &form.Redis{}
ginx.BindJsonAndValid(rc.GinCtx, form)
rc.ReqParam = form
redis := new(entity.Redis)
utils.Copy(redis, form)
redis.SetBaseInfo(rc.LoginAccount)
r.RedisApp.Save(redis)
}
func (r *Redis) DeleteRedis(rc *ctx.ReqCtx) {
r.RedisApp.Delete(uint64(ginx.PathParamInt(rc.GinCtx, "id")))
}
func (r *Redis) RedisInfo(rc *ctx.ReqCtx) {
res, _ := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(rc.GinCtx, "id"))).Cli.Info().Result()
datas := strings.Split(res, "\r\n")
i := 0
length := len(datas)
parseMap := make(map[string]map[string]string, 0)
for {
if i >= length {
break
}
if strings.Contains(datas[i], "#") {
key := utils.SubString(datas[i], strings.Index(datas[i], "#")+1, utils.StrLen(datas[i]))
i++
key = strings.Trim(key, " ")
sectionMap := make(map[string]string, 0)
for {
if i >= length || !strings.Contains(datas[i], ":") {
break
}
pair := strings.Split(datas[i], ":")
i++
if len(pair) != 2 {
continue
}
sectionMap[pair[0]] = pair[1]
}
parseMap[key] = sectionMap
} else {
i++
}
}
rc.ResData = parseMap
}
// scan获取redis的key列表信息
func (r *Redis) Scan(rc *ctx.ReqCtx) {
g := rc.GinCtx
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")))
keys, cursor := ri.Scan(uint64(ginx.PathParamInt(g, "cursor")), g.Query("match"), int64(ginx.PathParamInt(g, "count")))
var keyInfoSplit []string
if len(keys) > 0 {
keyInfoLua := `
local result = {}
-- KEYS[1]为第1个参数lua数组下标从1开始
local ttl = redis.call('ttl', KEYS[1]);
local keyType = redis.call('type', KEYS[1]);
for i = 1, #KEYS do
local ttl = redis.call('ttl', KEYS[i]);
local keyType = redis.call('type', KEYS[i]);
table.insert(result, string.format("%d,%s", ttl, keyType['ok']));
end;
return table.concat(result, ".");`
// 通过lua获取 ttl,type.ttl2,type2格式以便下面切割获取ttl和type。避免多次调用ttl和type函数
keyInfos, _ := ri.Cli.Eval(keyInfoLua, keys).Result()
keyInfoSplit = strings.Split(keyInfos.(string), ".")
}
kis := make([]*vo.KeyInfo, 0)
for i, k := range keys {
ttlType := strings.Split(keyInfoSplit[i], ",")
ttl, _ := strconv.Atoi(ttlType[0])
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: uint64(ttl)}
kis = append(kis, ki)
}
size, _ := ri.Cli.DBSize().Result()
rc.ResData = &vo.Keys{Cursor: cursor, Keys: kis, DbSize: size}
}
func (r *Redis) DeleteKey(rc *ctx.ReqCtx) {
g := rc.GinCtx
key := g.Query("key")
biz.NotEmpty(key, "key不能为空")
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")))
rc.ReqParam = key
ri.Cli.Del(key)
}
func (r *Redis) checkKey(rc *ctx.ReqCtx) (*application.RedisInstance, string) {
g := rc.GinCtx
key := g.Query("key")
biz.NotEmpty(key, "key不能为空")
return r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id"))), key
}
func (r *Redis) GetStringValue(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
str, err := ri.Cli.Get(key).Result()
biz.ErrIsNilAppendErr(err, "获取字符串值失败: %s")
rc.ResData = str
}
func (r *Redis) GetHashValue(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
res, err := ri.Cli.HGetAll(key).Result()
biz.ErrIsNilAppendErr(err, "获取hash值失败: %s")
rc.ResData = res
}
func (r *Redis) GetSetValue(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
res, err := ri.Cli.SMembers(key).Result()
biz.ErrIsNilAppendErr(err, "获取set值失败: %s")
rc.ResData = res
}
func (r *Redis) Test(rc *ctx.ReqCtx) {
schema := `{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Product",
"description": "A product from Acme's catalog",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a product",
"type": "integer"
},
"name": {
"description": "Name of the product",
"type": "string"
},
"price": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
}
},
"required": ["id", "name", "price"]
}
`
// 获取请求报文的内容长度
len := rc.GinCtx.Request.ContentLength
// 新建一个字节切片,长度与请求报文的内容长度相同
body := make([]byte, len)
// 读取 r 的请求主体,并将具体内容读入 body 中
rc.GinCtx.Request.Body.Read(body)
err := utils.ValidJsonString(schema, string(body))
biz.ErrIsNilAppendErr(err, "%s")
}
func (r *Redis) SetStringValue(rc *ctx.ReqCtx) {
g := rc.GinCtx
keyValue := new(form.KeyValue)
ginx.BindJsonAndValid(g, keyValue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")))
str, err := ri.Cli.Set(keyValue.Key, keyValue.Value, time.Second*time.Duration(keyValue.Timed)).Result()
biz.ErrIsNilAppendErr(err, "保存字符串值失败: %s")
rc.ResData = str
}

View File

@@ -10,6 +10,11 @@ type SelectDataDbVO struct {
Port *int `json:"port"`
Type *string `json:"type"`
Database *string `json:"database"`
Username *string `json:"username"`
ProjectId *int64 `json:"projectId"`
Project *string `json:"project"`
Env *string `json:"env"`
EnvId *int64 `json:"envId"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`

View File

@@ -0,0 +1,10 @@
package vo
// 用户选择项目
type AccountProject struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Remark string `json:"remark"`
}
type AccountProjects []AccountProject

View File

@@ -0,0 +1,29 @@
package vo
import "time"
type Redis struct {
Id *int64 `json:"id"`
// Name *string `json:"name"`
Host *string `json:"host"`
Db int `json:"db"`
ProjectId *int64 `json:"projectId"`
Project *string `json:"project"`
Env *string `json:"env"`
EnvId *int64 `json:"envId"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`
}
type Keys struct {
Cursor uint64 `json:"cursor"`
Keys []*KeyInfo `json:"keys"`
DbSize int64 `json:"dbSize"`
}
type KeyInfo struct {
Key string `json:"key"`
Ttl uint64 `json:"ttl"`
Type string `json:"type"`
}

View File

@@ -34,6 +34,7 @@ type MachineScriptVO struct {
Script *string `json:"script"`
Type *int `json:"type"`
Description *string `json:"description"`
Params *string `json:"params"`
MachineId *uint64 `json:"machineId"`
}

View File

@@ -1,16 +1,22 @@
package application
import (
"database/sql"
"errors"
"fmt"
"mayfly-go/base/biz"
"mayfly-go/base/model"
"mayfly-go/server/devops/domain/entity"
"mayfly-go/server/devops/domain/repository"
"mayfly-go/server/devops/infrastructure/db"
"mayfly-go/server/devops/infrastructure/persistence"
"strings"
"sync"
"time"
)
type IDb interface {
type Db interface {
// 分页获取机器脚本信息列表
GetPageList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult
GetPageList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 根据条件获取
GetDbBy(condition *entity.Db, cols ...string) error
@@ -20,36 +26,260 @@ type IDb interface {
Save(entity *entity.Db)
GetDbInstance(id uint64) *db.DbInstance
// 删除数据库信息
Delete(id uint64)
// 获取数据库连接实例
GetDbInstance(id uint64) *DbInstance
}
type dbApp struct {
dbRepo repository.Db
type dbAppImpl struct {
dbRepo repository.Db
dbSqlRepo repository.DbSql
}
var Db IDb = &dbApp{dbRepo: persistence.DbDao}
var DbApp Db = &dbAppImpl{
dbRepo: persistence.DbDao,
dbSqlRepo: persistence.DbSqlDao,
}
// 分页获取数据库信息列表
func (d *dbApp) GetPageList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult {
func (d *dbAppImpl) GetPageList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return d.dbRepo.GetDbList(condition, pageParam, toEntity, orderBy...)
}
// 根据条件获取
func (d *dbApp) GetDbBy(condition *entity.Db, cols ...string) error {
func (d *dbAppImpl) GetDbBy(condition *entity.Db, cols ...string) error {
return d.dbRepo.GetDb(condition, cols...)
}
// 根据id获取
func (d *dbApp) GetById(id uint64, cols ...string) *entity.Db {
func (d *dbAppImpl) GetById(id uint64, cols ...string) *entity.Db {
return d.dbRepo.GetById(id, cols...)
}
func (d *dbApp) Save(entity *entity.Db) {
func (d *dbAppImpl) Save(dbEntity *entity.Db) {
// 默认tcp连接
dbEntity.Network = "tcp"
// 测试连接
TestConnection(dbEntity)
// 查找是否存在该库
oldDb := &entity.Db{Host: dbEntity.Host, Port: dbEntity.Port, Database: dbEntity.Database}
err := d.GetDbBy(oldDb)
if dbEntity.Id == 0 {
biz.IsTrue(err != nil, "该库已存在")
d.dbRepo.Insert(dbEntity)
} else {
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(oldDb.Id == dbEntity.Id, "该库已存在")
}
// 先关闭数据库连接
CloseDb(dbEntity.Id)
d.dbRepo.Update(dbEntity)
}
}
func (d *dbApp) GetDbInstance(id uint64) *db.DbInstance {
return db.GetDbInstance(id, func(id uint64) *entity.Db {
return d.dbRepo.GetById(id)
})
func (d *dbAppImpl) Delete(id uint64) {
// 关闭连接
CloseDb(id)
d.dbRepo.Delete(id)
// 删除该库下用户保存的所有sql信息
d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: id})
}
func (da *dbAppImpl) GetDbInstance(id uint64) *DbInstance {
// Id不为0则为需要缓存
needCache := id != 0
if needCache {
load, ok := dbCache.Load(id)
if ok {
return load.(*DbInstance)
}
}
d := da.GetById(id)
biz.NotNil(d, "数据库信息不存在")
DB, err := sql.Open(d.Type, getDsn(d))
biz.ErrIsNil(err, fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err))
perr := DB.Ping()
if perr != nil {
panic(biz.NewBizErr(fmt.Sprintf("数据库连接失败: %s", perr.Error())))
}
// 最大连接周期超过时间的连接就close
DB.SetConnMaxLifetime(100 * time.Second)
// 设置最大连接数
DB.SetMaxOpenConns(2)
// 设置闲置连接数
DB.SetMaxIdleConns(1)
dbi := &DbInstance{Id: id, Type: d.Type, db: DB}
if needCache {
dbCache.LoadOrStore(d.Id, dbi)
}
return dbi
}
//------------------------------------------------------------------------------
var dbCache sync.Map
func GetDbInstanceByCache(id uint64) *DbInstance {
if load, ok := dbCache.Load(id); ok {
return load.(*DbInstance)
}
return nil
}
func TestConnection(d *entity.Db) {
biz.NotNil(d, "数据库信息不存在")
DB, err := sql.Open(d.Type, getDsn(d))
biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err)
defer DB.Close()
perr := DB.Ping()
biz.ErrIsNilAppendErr(perr, "数据库连接失败: %s")
}
// db实例
type DbInstance struct {
Id uint64
Type string
db *sql.DB
}
// 执行查询语句
// 依次返回 列名数组结果map错误
func (d *DbInstance) SelectData(sql string) ([]string, []map[string]string, error) {
sql = strings.Trim(sql, " ")
if !strings.HasPrefix(sql, "SELECT") && !strings.HasPrefix(sql, "select") {
return nil, nil, errors.New("该sql非查询语句")
}
// 没加limit则默认限制50条
if !strings.Contains(sql, "limit") && !strings.Contains(sql, "LIMIT") {
sql = sql + " LIMIT 50"
}
rows, err := d.db.Query(sql)
if err != nil {
return nil, nil, err
}
// rows对象一定要close掉如果出错不关掉则会很迅速的达到设置最大连接数
// 后面的链接过来直接报错或拒绝,实际上也没有起效果
defer func() {
if rows != nil {
rows.Close()
}
}()
cols, _ := rows.Columns()
// 这里表示一行填充数据
scans := make([]interface{}, len(cols))
// 这里表示一行所有列的值,用[]byte表示
vals := make([][]byte, len(cols))
// 这里scans引用vals把数据填充到[]byte里
for k := range vals {
scans[k] = &vals[k]
}
result := make([]map[string]string, 0)
// 列名
colNames := make([]string, 0)
// 是否第一次遍历,列名数组只需第一次遍历时加入
isFirst := true
for rows.Next() {
// 不Scan也会导致等待该链接实际处于未工作的状态然后也会导致连接数迅速达到最大
err := rows.Scan(scans...)
if err != nil {
return nil, nil, err
}
// 每行数据
rowData := make(map[string]string)
// 把vals中的数据复制到row中
for k, v := range vals {
key := cols[k]
// 如果是密码字段,则脱敏显示
if key == "password" {
v = []byte("******")
}
if isFirst {
colNames = append(colNames, key)
}
// 这里把[]byte数据转成string
rowData[key] = string(v)
}
//放入结果集
result = append(result, rowData)
isFirst = false
}
return colNames, result, nil
}
// 执行 update, insert, delete建表等sql
// 返回影响条数和错误
func (d *DbInstance) Exec(sql string) (int64, error) {
res, err := d.db.Exec(sql)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// 关闭连接,并从缓存中移除
func (d *DbInstance) Close() {
d.db.Close()
dbCache.Delete(d.Id)
}
// 获取dataSourceName
func getDsn(d *entity.Db) string {
if d.Type == "mysql" {
return fmt.Sprintf("%s:%s@%s(%s:%d)/%s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
}
return ""
}
func CloseDb(id uint64) {
if di := GetDbInstanceByCache(id); di != nil {
di.Close()
}
}
//-----------------------------------元数据-------------------------------------------
const (
// mysql 表信息元数据
MYSQL_TABLE_MA = `SELECT table_name tableName, engine, table_comment tableComment,
create_time createTime from information_schema.tables
WHERE table_schema = (SELECT database())`
// mysql 列信息元数据
MYSQL_COLOUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType,
column_comment columnComment, column_key columnKey, extra from information_schema.columns
WHERE table_name in (%s) AND table_schema = (SELECT database()) ORDER BY ordinal_position limit 15000`
)
func (d *DbInstance) GetTableMetedatas() []map[string]string {
var sql string
if d.Type == "mysql" {
sql = MYSQL_TABLE_MA
}
_, res, _ := d.SelectData(sql)
return res
}
func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]string {
var sql, tableName string
for i := 0; i < len(tableNames); i++ {
if i != 0 {
tableName = tableName + ", "
}
tableName = tableName + "'" + tableNames[i] + "'"
}
if d.Type == "mysql" {
sql = fmt.Sprintf(MYSQL_COLOUMN_MA, tableName)
}
_, res, err := d.SelectData(sql)
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
return res
}

View File

@@ -7,58 +7,95 @@ import (
"mayfly-go/server/devops/domain/repository"
"mayfly-go/server/devops/infrastructure/machine"
"mayfly-go/server/devops/infrastructure/persistence"
"gorm.io/gorm"
)
type IMachine interface {
type Machine interface {
// 根据条件获取账号信息
GetMachine(condition *entity.Machine, cols ...string) error
Save(entity *entity.Machine)
Delete(id uint64)
// 根据id获取
GetById(id uint64, cols ...string) *entity.Machine
// 分页获取机器信息列表
GetMachineList(condition *entity.Machine, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult
GetMachineList(condition *entity.Machine, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 获取机器连接
GetCli(id uint64) *machine.Cli
}
type machineApp struct {
type machineAppImpl struct {
machineRepo repository.Machine
}
var Machine IMachine = &machineApp{machineRepo: persistence.MachineDao}
var MachineApp Machine = &machineAppImpl{machineRepo: persistence.MachineDao}
// 分页获取机器信息列表
func (m *machineApp) GetMachineList(condition *entity.Machine, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult {
func (m *machineAppImpl) GetMachineList(condition *entity.Machine, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return m.machineRepo.GetMachineList(condition, pageParam, toEntity, orderBy...)
}
// 根据条件获取机器信息
func (m *machineApp) Save(entity *entity.Machine) {
biz.ErrIsNil(machine.TestConn(entity), "该机器无法连接")
if entity.Id != 0 {
m.machineRepo.UpdateById(entity)
func (m *machineAppImpl) Save(me *entity.Machine) {
biz.ErrIsNilAppendErr(machine.TestConn(me), "该机器无法连接: %s")
oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}
err := m.GetMachine(oldMachine)
if me.Id != 0 {
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(oldMachine.Id == me.Id, "该机器信息已存在")
}
// 关闭连接
machine.Close(me.Id)
m.machineRepo.UpdateById(me)
} else {
m.machineRepo.Create(entity)
biz.IsTrue(err != nil, "该机器信息已存在")
m.machineRepo.Create(me)
}
}
// 根据条件获取机器信息
func (m *machineApp) GetMachine(condition *entity.Machine, cols ...string) error {
func (m *machineAppImpl) Delete(id uint64) {
// 关闭连接
machine.Close(id)
model.Tx(
func(db *gorm.DB) error {
// 删除machine表信息
return db.Delete(new(entity.Machine), "id = ?", id).Error
},
func(db *gorm.DB) error {
// 删除machine_file
machineFile := &entity.MachineFile{MachineId: id}
return db.Where(machineFile).Delete(machineFile).Error
},
func(db *gorm.DB) error {
// 删除machine_script
machineScript := &entity.MachineScript{MachineId: id}
return db.Where(machineScript).Delete(machineScript).Error
},
)
}
// 根据条件获取机器信息
func (m *machineAppImpl) GetMachine(condition *entity.Machine, cols ...string) error {
return m.machineRepo.GetMachine(condition, cols...)
}
func (m *machineApp) GetById(id uint64, cols ...string) *entity.Machine {
func (m *machineAppImpl) GetById(id uint64, cols ...string) *entity.Machine {
return m.machineRepo.GetById(id, cols...)
}
func (m *machineApp) GetCli(id uint64) *machine.Cli {
func (m *machineAppImpl) GetCli(id uint64) *machine.Cli {
cli, err := machine.GetCli(id, func(machineId uint64) *entity.Machine {
return m.GetById(machineId)
})
biz.ErrIsNil(err, "获取客户端错误")
biz.ErrIsNilAppendErr(err, "获取客户端错误: %s")
return cli
}

View File

@@ -15,9 +15,9 @@ import (
"github.com/pkg/sftp"
)
type IMachineFile interface {
type MachineFile interface {
// 分页获取机器文件信息列表
GetPageList(condition *entity.MachineFile, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult
GetPageList(condition *entity.MachineFile, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 根据条件获取
GetMachineFile(condition *entity.MachineFile, cols ...string) error
@@ -47,34 +47,34 @@ type IMachineFile interface {
RemoveFile(fileId uint64, path string)
}
type machineFileApp struct {
type machineFileAppImpl struct {
machineFileRepo repository.MachineFile
machineRepo repository.Machine
}
// 实现类单例
var MachineFile IMachineFile = &machineFileApp{
var MachineFileApp MachineFile = &machineFileAppImpl{
machineRepo: persistence.MachineDao,
machineFileRepo: persistence.MachineFileDao,
}
// 分页获取机器脚本信息列表
func (m *machineFileApp) GetPageList(condition *entity.MachineFile, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult {
func (m *machineFileAppImpl) GetPageList(condition *entity.MachineFile, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return m.machineFileRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
// 根据条件获取
func (m *machineFileApp) GetMachineFile(condition *entity.MachineFile, cols ...string) error {
func (m *machineFileAppImpl) GetMachineFile(condition *entity.MachineFile, cols ...string) error {
return m.machineFileRepo.GetMachineFile(condition, cols...)
}
// 根据id获取
func (m *machineFileApp) GetById(id uint64, cols ...string) *entity.MachineFile {
func (m *machineFileAppImpl) GetById(id uint64, cols ...string) *entity.MachineFile {
return m.machineFileRepo.GetById(id, cols...)
}
// 保存机器文件配置
func (m *machineFileApp) Save(entity *entity.MachineFile) {
func (m *machineFileAppImpl) Save(entity *entity.MachineFile) {
biz.NotNil(m.machineRepo.GetById(entity.MachineId, "Name"), "该机器不存在")
if entity.Id != 0 {
@@ -85,11 +85,11 @@ func (m *machineFileApp) Save(entity *entity.MachineFile) {
}
// 根据id删除
func (m *machineFileApp) Delete(id uint64) {
func (m *machineFileAppImpl) Delete(id uint64) {
m.machineFileRepo.Delete(id)
}
func (m *machineFileApp) ReadDir(fid uint64, path string) []fs.FileInfo {
func (m *machineFileAppImpl) ReadDir(fid uint64, path string) []fs.FileInfo {
path, machineId := m.checkAndReturnPathMid(fid, path)
if !strings.HasSuffix(path, "/") {
path = path + "/"
@@ -101,7 +101,7 @@ func (m *machineFileApp) ReadDir(fid uint64, path string) []fs.FileInfo {
return fis
}
func (m *machineFileApp) ReadFile(fileId uint64, path string) ([]byte, fs.FileInfo) {
func (m *machineFileAppImpl) ReadFile(fileId uint64, path string) ([]byte, fs.FileInfo) {
path, machineId := m.checkAndReturnPathMid(fileId, path)
sftpCli := m.getSftpCli(machineId)
// 读取文件内容
@@ -119,7 +119,7 @@ func (m *machineFileApp) ReadFile(fileId uint64, path string) ([]byte, fs.FileIn
}
// 写文件内容
func (m *machineFileApp) WriteFileContent(fileId uint64, path string, content []byte) {
func (m *machineFileAppImpl) WriteFileContent(fileId uint64, path string, content []byte) {
_, machineId := m.checkAndReturnPathMid(fileId, path)
sftpCli := m.getSftpCli(machineId)
@@ -133,7 +133,7 @@ func (m *machineFileApp) WriteFileContent(fileId uint64, path string, content []
}
// 上传文件
func (m *machineFileApp) UploadFile(fileId uint64, path, filename string, content []byte) {
func (m *machineFileAppImpl) UploadFile(fileId uint64, path, filename string, content []byte) {
path, machineId := m.checkAndReturnPathMid(fileId, path)
if !strings.HasSuffix(path, "/") {
path = path + "/"
@@ -148,7 +148,7 @@ func (m *machineFileApp) UploadFile(fileId uint64, path, filename string, conten
}
// 删除文件
func (m *machineFileApp) RemoveFile(fileId uint64, path string) {
func (m *machineFileAppImpl) RemoveFile(fileId uint64, path string) {
path, machineId := m.checkAndReturnPathMid(fileId, path)
sftpCli := m.getSftpCli(machineId)
@@ -164,12 +164,12 @@ func (m *machineFileApp) RemoveFile(fileId uint64, path string) {
}
// 获取sftp client
func (m *machineFileApp) getSftpCli(machineId uint64) *sftp.Client {
return Machine.GetCli(machineId).GetSftpCli()
func (m *machineFileAppImpl) getSftpCli(machineId uint64) *sftp.Client {
return MachineApp.GetCli(machineId).GetSftpCli()
}
// 校验并返回实际可访问的文件path
func (m *machineFileApp) checkAndReturnPathMid(fid uint64, inputPath string) (string, uint64) {
func (m *machineFileAppImpl) checkAndReturnPathMid(fid uint64, inputPath string) (string, uint64) {
biz.IsTrue(fid != 0, "文件id不能为空")
mf := m.GetById(uint64(fid))
biz.NotNil(mf, "文件不存在")

View File

@@ -8,9 +8,9 @@ import (
"mayfly-go/server/devops/infrastructure/persistence"
)
type IMachineScript interface {
type MachineScript interface {
// 分页获取机器脚本信息列表
GetPageList(condition *entity.MachineScript, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult
GetPageList(condition *entity.MachineScript, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 根据条件获取
GetMachineScript(condition *entity.MachineScript, cols ...string) error
@@ -23,7 +23,7 @@ type IMachineScript interface {
Delete(id uint64)
}
type machineScriptApp struct {
type machineScriptAppImpl struct {
machineScriptRepo repository.MachineScript
machineRepo repository.Machine
}
@@ -31,27 +31,27 @@ type machineScriptApp struct {
const Common_Script_Machine_Id = 9999999
// 实现类单例
var MachineScript IMachineScript = &machineScriptApp{
var MachineScriptApp MachineScript = &machineScriptAppImpl{
machineRepo: persistence.MachineDao,
machineScriptRepo: persistence.MachineScriptDao}
// 分页获取机器脚本信息列表
func (m *machineScriptApp) GetPageList(condition *entity.MachineScript, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult {
func (m *machineScriptAppImpl) GetPageList(condition *entity.MachineScript, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return m.machineScriptRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
// 根据条件获取
func (m *machineScriptApp) GetMachineScript(condition *entity.MachineScript, cols ...string) error {
func (m *machineScriptAppImpl) GetMachineScript(condition *entity.MachineScript, cols ...string) error {
return m.machineScriptRepo.GetMachineScript(condition, cols...)
}
// 根据id获取
func (m *machineScriptApp) GetById(id uint64, cols ...string) *entity.MachineScript {
func (m *machineScriptAppImpl) GetById(id uint64, cols ...string) *entity.MachineScript {
return m.machineScriptRepo.GetById(id, cols...)
}
// 保存机器脚本
func (m *machineScriptApp) Save(entity *entity.MachineScript) {
func (m *machineScriptAppImpl) Save(entity *entity.MachineScript) {
// 如果机器id不为公共脚本id则校验机器是否存在
if machineId := entity.MachineId; machineId != Common_Script_Machine_Id {
biz.NotNil(m.machineRepo.GetById(machineId, "Name"), "该机器不存在")
@@ -65,6 +65,6 @@ func (m *machineScriptApp) Save(entity *entity.MachineScript) {
}
// 根据id删除
func (m *machineScriptApp) Delete(id uint64) {
func (m *machineScriptAppImpl) Delete(id uint64) {
m.machineScriptRepo.Delete(id)
}

View File

@@ -0,0 +1,95 @@
package application
import (
"mayfly-go/base/biz"
"mayfly-go/base/model"
"mayfly-go/server/devops/domain/entity"
"mayfly-go/server/devops/domain/repository"
"mayfly-go/server/devops/infrastructure/persistence"
)
type Project interface {
// 分页获取项目信息列表
GetPageList(condition *entity.Project, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
ListProjectByIds(ids []uint64, toEntity interface{}, orderBy ...string)
SaveProject(project *entity.Project)
// 根据项目id获取所有该项目下的环境信息列表
ListEnvByProjectId(projectId uint64, listPtr interface{})
// 保存项目环境信息
SaveProjectEnv(projectEnv *entity.ProjectEnv)
// 根据条件获取项目成员信息
ListMember(condition *entity.ProjectMember, toEntity interface{}, orderBy ...string)
SaveProjectMember(pm *entity.ProjectMember)
// 根据条件获取项目成员信息
GetMemberPage(condition *entity.ProjectMember, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
DeleteMember(projectId, accountId uint64)
}
type projectAppImpl struct {
projectRepo repository.Project
projectEnvRepo repository.ProjectEnv
projectMemberRepo repository.ProjectMemeber
}
var ProjectApp Project = &projectAppImpl{
projectRepo: persistence.ProjectRepo,
projectEnvRepo: persistence.ProjectEnvRepo,
projectMemberRepo: persistence.ProjectMemberRepo,
}
// 分页获取项目信息列表
func (p *projectAppImpl) GetPageList(condition *entity.Project, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return p.projectRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (p *projectAppImpl) ListProjectByIds(ids []uint64, toEntity interface{}, orderBy ...string) {
p.projectRepo.GetByIdIn(ids, toEntity, orderBy...)
}
func (p *projectAppImpl) SaveProject(project *entity.Project) {
if project.Id == 0 {
p.projectRepo.Save(project)
} else {
// 防止误传导致项目名更新
project.Name = ""
p.projectRepo.Update(project)
}
}
// 根据项目id获取所有该项目下的环境信息列表
func (p *projectAppImpl) ListEnvByProjectId(projectId uint64, listPtr interface{}) {
p.projectEnvRepo.ListEnv(&entity.ProjectEnv{ProjectId: projectId}, listPtr)
}
// 保存项目环境信息
func (p *projectAppImpl) SaveProjectEnv(projectEnv *entity.ProjectEnv) {
p.projectEnvRepo.Save(projectEnv)
}
// 根据条件获取项目成员信息
func (p *projectAppImpl) ListMember(condition *entity.ProjectMember, toEntity interface{}, orderBy ...string) {
p.projectMemberRepo.ListMemeber(condition, toEntity, orderBy...)
}
func (p *projectAppImpl) SaveProjectMember(pm *entity.ProjectMember) {
pms := new([]entity.ProjectMember)
p.ListMember(&entity.ProjectMember{ProjectId: pm.ProjectId, AccountId: pm.AccountId}, pms)
biz.IsTrue(len(*pms) == 0, "该成员已存在")
p.projectMemberRepo.Save(pm)
}
func (p *projectAppImpl) GetMemberPage(condition *entity.ProjectMember, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return p.projectMemberRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (p *projectAppImpl) DeleteMember(projectId, accountId uint64) {
p.projectMemberRepo.DeleteByPidMid(projectId, accountId)
}

View File

@@ -0,0 +1,146 @@
package application
import (
"mayfly-go/base/biz"
"mayfly-go/base/model"
"mayfly-go/server/devops/domain/entity"
"mayfly-go/server/devops/domain/repository"
"mayfly-go/server/devops/infrastructure/persistence"
"sync"
"github.com/go-redis/redis"
)
type Redis interface {
// 分页获取机器脚本信息列表
GetPageList(condition *entity.Redis, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 根据id获取
GetById(id uint64, cols ...string) *entity.Redis
// 根据条件获取
GetRedisBy(condition *entity.Redis, cols ...string) error
Save(entity *entity.Redis)
// 删除数据库信息
Delete(id uint64)
// 获取数据库连接实例
GetRedisInstance(id uint64) *RedisInstance
}
type redisAppImpl struct {
redisRepo repository.Redis
}
var RedisApp Redis = &redisAppImpl{
redisRepo: persistence.RedisDao,
}
// 分页获取机器脚本信息列表
func (r *redisAppImpl) GetPageList(condition *entity.Redis, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return r.redisRepo.GetRedisList(condition, pageParam, toEntity, orderBy...)
}
// 根据id获取
func (r *redisAppImpl) GetById(id uint64, cols ...string) *entity.Redis {
return r.redisRepo.GetById(id, cols...)
}
// 根据条件获取
func (r *redisAppImpl) GetRedisBy(condition *entity.Redis, cols ...string) error {
return r.redisRepo.GetRedis(condition, cols...)
}
func (r *redisAppImpl) Save(re *entity.Redis) {
TestRedisConnection(re)
// 查找是否存在该库
oldRedis := &entity.Redis{Host: re.Host, Db: re.Db}
err := r.GetRedisBy(oldRedis)
if re.Id == 0 {
biz.IsTrue(err != nil, "该库已存在")
r.redisRepo.Insert(re)
} else {
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(re.Id == re.Id, "该库已存在")
}
// 先关闭数据库连接
CloseRedis(re.Id)
r.redisRepo.Update(re)
}
}
// 删除Redis信息
func (r *redisAppImpl) Delete(id uint64) {
CloseRedis(id)
r.redisRepo.Delete(id)
}
// 获取数据库连接实例
func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
// Id不为0则为需要缓存
needCache := id != 0
if needCache {
load, ok := redisCache.Load(id)
if ok {
return load.(*RedisInstance)
}
}
// 缓存不存在则回调获取redis信息
re := r.GetById(id)
biz.NotNil(re, "redis信息不存在")
rcli := redis.NewClient(&redis.Options{
Addr: re.Host,
Password: re.Password, // no password set
DB: re.Db, // use default DB
})
// 测试连接
_, e := rcli.Ping().Result()
biz.ErrIsNilAppendErr(e, "redis连接失败: %s")
ri := &RedisInstance{Id: id, Cli: rcli}
if needCache {
redisCache.LoadOrStore(re.Id, ri)
}
return ri
}
//------------------------------------------------------------------------------
var redisCache sync.Map
// redis实例
type RedisInstance struct {
Id uint64
Cli *redis.Client
}
// 关闭redis连接
func CloseRedis(id uint64) {
if load, ok := redisCache.Load(id); ok {
load.(*RedisInstance).Cli.Close()
redisCache.Delete(id)
}
}
func TestRedisConnection(re *entity.Redis) {
rcli := redis.NewClient(&redis.Options{
Addr: re.Host,
Password: re.Password, // no password set
DB: re.Db, // use default DB
})
defer rcli.Close()
// 测试连接
_, e := rcli.Ping().Result()
biz.ErrIsNilAppendErr(e, "Redis连接失败: %s")
}
func (r *RedisInstance) Scan(cursor uint64, match string, count int64) ([]string, uint64) {
keys, newcursor, err := r.Cli.Scan(cursor, match, count).Result()
biz.ErrIsNilAppendErr(err, "scan失败: %s")
return keys, newcursor
}

View File

@@ -7,12 +7,16 @@ import (
type Db 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:"-"`
Database string `orm:"column(database)" json:"database"`
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:"-"`
Database string `orm:"column(database)" json:"database"`
ProjectId uint64
Project string
EnvId uint64
Env string
}

View File

@@ -4,12 +4,10 @@ import "mayfly-go/base/model"
type MachineScript struct {
model.Model
Name string `json:"name"`
// 机器id
MachineId uint64 `json:"machineId"`
Type int `json:"type"`
// 脚本内容
Description string `json:"description"`
// 脚本内容
Script string `json:"script"`
Name string `json:"name"`
MachineId uint64 `json:"machineId"` // 机器id
Type int `json:"type"`
Description string `json:"description"` // 脚本描述
Params string `json:"params"` // 参数列表json
Script string `json:"script"` // 脚本内容
}

View File

@@ -0,0 +1,10 @@
package entity
import "mayfly-go/base/model"
// 项目
type Project struct {
model.Model
Name string `json:"name"` // 项目名
Remark string `json:"remark"` // 备注说明
}

View File

@@ -0,0 +1,11 @@
package entity
import "mayfly-go/base/model"
// 项目环境
type ProjectEnv struct {
model.Model
Name string `json:"name"` // 环境名
ProjectId uint64 `json:"projectId"` // 项目id
Remark string `json:"remark"` // 备注说明
}

View File

@@ -0,0 +1,11 @@
package entity
import "mayfly-go/base/model"
// 项目成员,用于对项目下组件的访问控制
type ProjectMember struct {
model.Model
AccountId uint64 `json:"accountId"` // 账号
Username string `json:"username"` // 账号用户名
ProjectId uint64 `json:"projectId"` // 项目id
}

View File

@@ -0,0 +1,17 @@
package entity
import (
"mayfly-go/base/model"
)
type Redis struct {
model.Model
Host string `orm:"column(host)" json:"host"`
Password string `orm:"column(password)" json:"-"`
Db int `orm:"column(database)" json:"db"`
ProjectId uint64
Project string
EnvId uint64
Env string
}

View File

@@ -7,11 +7,17 @@ import (
type Db interface {
// 分页获取机器信息列表
GetDbList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) model.PageResult
GetDbList(condition *entity.Db, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
// 根据条件获取账号信息
GetDb(condition *entity.Db, cols ...string) error
// 根据id获取
GetById(id uint64, cols ...string) *entity.Db
Insert(db *entity.Db)
Update(db *entity.Db)
Delete(id uint64)
}

View File

@@ -0,0 +1,7 @@
package repository
import "mayfly-go/server/devops/domain/entity"
type DbSql interface {
DeleteBy(condition *entity.DbSql)
}

Some files were not shown because too many files have changed in this diff Show More