fix: Redis查询核心bug修复 — 命令拆分+HGETALL结构化+DB号+插入换行

1. executeRedisQuery: client.Do()没传命令名导致ERR unknown command
2. HGETALL结果用reflect处理map类型,结构化输出field/value表格
3. 支持指定Redis DB号(之前固定DB=0)
4. 前端Redis命令插入用换行分隔,MySQL保持空格
5. cost_ms到execution_time_ms字段映射修复
This commit is contained in:
logan committed 2026-05-28 14:53:12 +08:00
1 parent 6b5d8bede4
commit 828a0597c5
2 files changed
+97 -17

No files matched your search

+88 -12
View File
@@ -3,6 +3,7 @@ package handler
import (
"fmt"
"net/http"
"reflect"
"sql-platform/internal/model"
@@ -12,6 +13,7 @@ import (
"github.com/redis/go-redis/v9"
"sql-platform/internal/middleware"
"strings"
"strconv"
)
// GET /api/v1/instances/:id/detail/databases
@@ -533,13 +535,14 @@ func ExecuteQuery(c *gin.Context) {
c.JSON(http.StatusOK, result)
case "redis":
result, err := executeRedisQuery(&instance, input.SQL)
result, err := executeRedisQuery(&instance, input.Database, input.SQL)
costMs := time.Since(start).Milliseconds()
if err != nil {
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": result, "cost_ms": costMs, "columns": []string{}, "rows": []interface{}{}, "total": 0})
saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, 1)
c.JSON(http.StatusOK, result)
default:
c.JSON(http.StatusBadRequest, gin.H{"detail": "Unsupported database type: " + instance.DBType})
@@ -593,7 +596,7 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string) (gi
}, nil
}
func executeRedisQuery(instance *model.Instance, command string) (string, error) {
func executeRedisQuery(instance *model.Instance, dbSelect string, command string) (gin.H, error) {
password := instance.Password
if strings.HasPrefix(password, "enc:") {
decrypted, err := decryptPassword(strings.TrimPrefix(password, "enc:"))
@@ -601,21 +604,30 @@ func executeRedisQuery(instance *model.Instance, command string) (string, error)
password = decrypted
}
}
// Parse Redis DB number from dbSelect (e.g. "0", "1")
redisDB := 0
if dbSelect != "" {
if n, err := strconv.Atoi(dbSelect); err == nil && n >= 0 {
redisDB = n
}
}
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", instance.Host, instance.Port),
Password: password,
DB: 0,
DB: redisDB,
})
defer client.Close()
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
return "", fmt.Errorf("Redis connection failed: %v", err)
return nil, fmt.Errorf("Redis connection failed: %v", err)
}
parts := strings.Fields(command)
if len(parts) == 0 {
return "", fmt.Errorf("empty command")
return nil, fmt.Errorf("empty command")
}
cmd := strings.ToUpper(parts[0])
@@ -625,20 +637,84 @@ func executeRedisQuery(instance *model.Instance, command string) (string, error)
"TYPE": true, "TTL": true, "EXISTS": true, "KEYS": true,
"INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true,
"LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true,
"HKEYS": true, "HVALS": true, "HSCAN": true, "SSCAN": true,
"ZSCAN": true, "ZRANGEBYSCORE": true,
"GETSET": true, "GETRANGE": true, "SUBSTR": true,
}
if !readOnly[cmd] {
return "", fmt.Errorf("only read-only commands allowed, got: %s", cmd)
return nil, fmt.Errorf("only read-only commands allowed, got: %s", cmd)
}
args := make([]interface{}, len(parts)-1)
for i, p := range parts[1:] {
args[i] = p
// Build args: command name + all arguments
args := make([]interface{}, 0, len(parts))
args = append(args, cmd)
for _, p := range parts[1:] {
args = append(args, p)
}
result, err := client.Do(ctx, args...).Result()
if err != nil {
return "", fmt.Errorf("command failed: %v", err)
return nil, fmt.Errorf("command failed: %v", err)
}
return fmt.Sprintf("%v", result), nil
// Format result into rows/columns for frontend table display
columns := []string{"key", "value"}
var rows []interface{}
switch v := result.(type) {
case []interface{}:
// Array result (MGET, LRANGE, etc.)
if cmd == "MGET" {
for i, val := range v {
rows = append(rows, map[string]interface{}{
"index": i,
"value": fmt.Sprintf("%v", val),
})
}
columns = []string{"index", "value"}
} else {
for i, val := range v {
rows = append(rows, map[string]interface{}{
"index": i,
"value": fmt.Sprintf("%v", val),
})
}
}
case map[string]string:
// HGETALL returns map[string]string via some redis clients
for k, val := range v {
rows = append(rows, map[string]interface{}{
"field": k,
"value": val,
})
}
columns = []string{"field", "value"}
default:
// Use reflect to handle any map type (map[interface{}]interface{}, etc.)
rv := reflect.ValueOf(result)
if rv.Kind() == reflect.Map {
iter := rv.MapRange()
for iter.Next() {
rows = append(rows, map[string]interface{}{
"field": fmt.Sprintf("%v", iter.Key().Interface()),
"value": fmt.Sprintf("%v", iter.Value().Interface()),
})
}
columns = []string{"field", "value"}
} else {
rows = append(rows, map[string]interface{}{
"command": command,
"result": fmt.Sprintf("%v", v),
})
columns = []string{"command", "result"}
}
}
return gin.H{
"columns": columns,
"rows": rows,
"total": len(rows),
}, nil
}
func saveQueryHistory(userID int, instanceID int, database string, sqlStr string, costMs int64, rowCount int) {