Files
mayfly-go/server/internal/redis/api/redis.go
zongyangleo 142bbd265d !134 feat: 新增支持es和连接池
* feat: 各连接,支持连接池
* feat:支持es
2025-05-21 04:42:30 +00:00

296 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"mayfly-go/internal/pkg/consts"
"mayfly-go/internal/pkg/utils"
"mayfly-go/internal/redis/api/form"
"mayfly-go/internal/redis/api/vo"
"mayfly-go/internal/redis/application"
"mayfly-go/internal/redis/application/dto"
"mayfly-go/internal/redis/domain/entity"
"mayfly-go/internal/redis/imsg"
"mayfly-go/internal/redis/rdm"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"strings"
"github.com/may-fly/cast"
"github.com/redis/go-redis/v9"
)
type Redis struct {
redisApp application.Redis `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
func (rs *Redis) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取redis list
req.NewGet("", rs.RedisList),
req.NewPost("/test-conn", rs.TestConn),
req.NewPost("", rs.Save).Log(req.NewLogSaveI(imsg.LogRedisSave)),
req.NewDelete(":id", rs.DeleteRedis).Log(req.NewLogSaveI(imsg.LogRedisDelete)),
req.NewGet("/:id/info", rs.RedisInfo),
req.NewGet(":id/cluster-info", rs.ClusterInfo),
req.NewPost(":id/:db/run-cmd", rs.RunCmd).Log(req.NewLogSaveI(imsg.LogRedisRunCmd)),
// 获取指定redis keys
req.NewPost(":id/:db/scan", rs.ScanKeys),
req.NewGet(":id/:db/key-info", rs.KeyInfo),
req.NewGet(":id/:db/key-ttl", rs.TtlKey),
req.NewGet(":id/:db/key-memuse", rs.MemoryUsage),
}
return req.NewConfs("/redis", reqs[:]...)
}
func (r *Redis) RedisList(rc *req.Ctx) {
queryCond := req.BindQuery(rc, new(entity.RedisQuery))
// 不存在可访问标签id即没有可操作数据
tags := r.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeRedis)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
if len(tags) == 0 {
rc.ResData = model.NewEmptyPageResult[any]()
return
}
queryCond.Codes = tags.GetCodes()
res, err := r.redisApp.GetPageList(queryCond)
biz.ErrIsNil(err)
resVo := model.PageResultConv[*entity.Redis, *vo.Redis](res)
redisvos := resVo.List
// 填充标签信息
r.tagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeRedis), collx.ArrayMap(redisvos, func(rvo *vo.Redis) tagentity.ITagResource {
return rvo
})...)
rc.ResData = resVo
}
func (r *Redis) TestConn(rc *req.Ctx) {
form := &form.Redis{}
redis := req.BindJsonAndCopyTo[*entity.Redis](rc, form, new(entity.Redis))
authCert := &tagentity.ResourceAuthCert{
Username: form.Username,
Ciphertext: form.Password,
CiphertextType: tagentity.AuthCertCiphertextTypePassword,
Type: tagentity.AuthCertTypePrivate,
}
if form.Mode == string(rdm.SentinelMode) {
encPwd, err := utils.PwdAesEncrypt(form.RedisNodePassword)
biz.ErrIsNil(err)
authCert.SetExtraValue("redisNodePassword", encPwd)
}
biz.ErrIsNil(r.redisApp.TestConn(&dto.SaveRedis{
Redis: redis,
AuthCert: authCert,
}))
}
func (r *Redis) Save(rc *req.Ctx) {
form := &form.Redis{}
redis := req.BindJsonAndCopyTo[*entity.Redis](rc, form, new(entity.Redis))
redisParam := &dto.SaveRedis{
Redis: redis,
TagCodePaths: form.TagCodePaths,
}
authCert := &tagentity.ResourceAuthCert{
Username: form.Username,
Ciphertext: form.Password,
CiphertextType: tagentity.AuthCertCiphertextTypePassword,
Type: tagentity.AuthCertTypePrivate,
}
if form.Mode == string(rdm.SentinelMode) {
encPwd, err := utils.PwdAesEncrypt(form.RedisNodePassword)
biz.ErrIsNil(err)
authCert.SetExtraValue("redisNodePassword", encPwd)
}
redisParam.AuthCert = authCert
// 密码脱敏记录日志
form.Password = "****"
rc.ReqParam = form
biz.ErrIsNil(r.redisApp.SaveRedis(rc.MetaCtx, redisParam))
}
func (r *Redis) DeleteRedis(rc *req.Ctx) {
idsStr := rc.PathParam("id")
rc.ReqParam = idsStr
ids := strings.Split(idsStr, ",")
for _, v := range ids {
r.redisApp.Delete(rc.MetaCtx, cast.ToUint64(v))
}
}
func (r *Redis) RedisInfo(rc *req.Ctx) {
ri, err := r.redisApp.GetRedisConn(uint64(rc.PathParamInt("id")), 0)
biz.ErrIsNil(err)
defer rdm.PutRedisConn(ri)
section := rc.Query("section")
mode := ri.Info.Mode
ctx := context.Background()
var redisCli *redis.Client
if mode == "" || mode == rdm.StandaloneMode || mode == rdm.SentinelMode {
redisCli = ri.Cli
} else if mode == rdm.ClusterMode {
host := rc.Query("host")
biz.NotEmpty(host, "the cluster mode host info cannot be empty")
clusterClient := ri.ClusterCli
// 遍历集群的master节点找到该redis client
clusterClient.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {
if host == client.Options().Addr {
redisCli = client
}
return nil
})
if redisCli == nil {
// 遍历集群的slave节点找到该redis client
clusterClient.ForEachSlave(ctx, func(ctx context.Context, client *redis.Client) error {
if host == client.Options().Addr {
redisCli = client
}
return nil
})
}
biz.NotNil(redisCli, "the instance is not in the cluster")
}
var res string
if section == "" {
res, err = redisCli.Info(ctx).Result()
} else {
res, err = redisCli.Info(ctx, section).Result()
}
biz.ErrIsNilAppendErr(err, "get redis info error: %s")
datas := strings.Split(res, "\r\n")
i := 0
length := len(datas)
parseMap := make(map[string]map[string]string)
for {
if i >= length {
break
}
if strings.Contains(datas[i], "#") {
key := stringx.SubString(datas[i], strings.Index(datas[i], "#")+1, stringx.Len(datas[i]))
i++
key = strings.Trim(key, " ")
sectionMap := make(map[string]string)
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
}
func (r *Redis) ClusterInfo(rc *req.Ctx) {
ri, err := r.redisApp.GetRedisConn(uint64(rc.PathParamInt("id")), 0)
biz.ErrIsNil(err)
defer rdm.PutRedisConn(ri)
biz.IsEquals(ri.Info.Mode, rdm.ClusterMode, "non-cluster mode")
info, _ := ri.ClusterCli.ClusterInfo(context.Background()).Result()
nodesStr, _ := ri.ClusterCli.ClusterNodes(context.Background()).Result()
nodesRes := make([]map[string]string, 0)
nodes := strings.Split(nodesStr, "\n")
for _, node := range nodes {
if node == "" {
continue
}
nodeInfos := strings.Split(node, " ")
node := make(map[string]string)
node["nodeId"] = nodeInfos[0]
// ip:port1@port2port1指redis服务器与客户端通信的端口port2则是集群内部节点间通信的端口
node["ip"] = nodeInfos[1]
node["flags"] = nodeInfos[2]
// 如果节点是slave并且已知master节点则为master节点ID否则为符号"-"
node["masterSlaveRelation"] = nodeInfos[3]
// 最近一次发送ping的时间这个时间是一个unix毫秒时间戳0代表没有发送过
node["pingSent"] = nodeInfos[4]
// 最近一次收到pong的时间使用unix时间戳表示
node["pongRecv"] = nodeInfos[5]
// 节点的epoch值如果该节点是从节点则为其主节点的epoch值。每当节点发生失败切换时都会创建一个新的独特的递增的epoch。
// 如果多个节点竞争同一个哈希槽时epoch值更高的节点会抢夺到
node["configEpoch"] = nodeInfos[6]
// node-to-node集群总线使用的链接的状态我们使用这个链接与集群中其他节点进行通信.值可以是 connected 和 disconnected
node["linkState"] = nodeInfos[7]
// slave节点没有插槽信息
if len(nodeInfos) > 8 {
// slotmaster节点第9位为哈希槽值或者一个哈希槽范围代表当前节点可以提供服务的所有哈希槽值。如果只是一个值,那就是只有一个槽会被使用。
// 如果是一个范围,这个值表示为起始槽-结束槽,节点将处理包括起始槽和结束槽在内的所有哈希槽。
node["slot"] = nodeInfos[8]
}
nodesRes = append(nodesRes, node)
}
rc.ResData = collx.M{
"clusterInfo": info,
"clusterNodes": nodesRes,
}
}
// 校验查询参数中的key为必填项并返回redis实例
func (r *Redis) checkKeyAndGetRedisConn(rc *req.Ctx) (*rdm.RedisConn, string) {
key := rc.Query("key")
biz.NotEmpty(key, "key cannot be empty")
return r.getRedisConn(rc), key
}
func (r *Redis) getRedisConn(rc *req.Ctx) *rdm.RedisConn {
ri, err := r.redisApp.GetRedisConn(getIdAndDbNum(rc))
biz.ErrIsNil(err)
defer rdm.PutRedisConn(ri)
biz.ErrIsNilAppendErr(r.tagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.CodePath...), "%s")
return ri
}
// 获取redis id与要操作的库号统一路径
func getIdAndDbNum(rc *req.Ctx) (uint64, int) {
return uint64(rc.PathParamInt("id")), rc.PathParamInt("db")
}