diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index 93efe68..66a93c3 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -14,6 +14,7 @@ import ( "sql-platform/internal/model" "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" ) // SqlExecItem records the execution result of a single SQL statement @@ -571,6 +572,33 @@ func ExecuteTicket(c *gin.Context) { } execStart := time.Now() result, err := client.Do(ctx, args...).Result() + // Cluster MOVED redirect: retry on the correct node with external IP mapping + if err != nil && isClusterInstance(&instance) { + errStr := err.Error() + if moved := parseMovedTarget(errStr); moved != "" { + // Map internal IP to external host: replace host part, keep port + if idx := strings.LastIndex(moved, ":"); idx >= 0 { + port := moved[idx+1:] + externalAddr := instance.Host + ":" + port + password := instance.Password + if strings.HasPrefix(password, "enc:") { + if dec, e := decryptPassword(strings.TrimPrefix(password, "enc:")); e == nil { + password = dec + } + } + targetClient := redis.NewClient(&redis.Options{ + Addr: externalAddr, + Username: instance.Username, + Password: password, + }) + defer targetClient.Close() + result, err = targetClient.Do(ctx, args...).Result() + if err != nil { + err = fmt.Errorf("集群节点重定向失败(MOVED→%s): %w — 集群节点端口(%s)可能未暴露给外部,需检查Docker端口映射", moved, err, port) + } + } + } + } stmtMs := time.Since(execStart).Milliseconds() execRes := SqlExecResult{ Status: "success", @@ -659,36 +687,42 @@ func ExecuteTicket(c *gin.Context) { execTimeout := getSettingInt("executor_timeout", 300) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second) defer cancel() - // Dry-run: EXPLAIN pre-check (skip DDL statements) + // Dry-run: EXPLAIN pre-check if getSetting("executor_dry_run", "true") == "true" { ddlPrefixes := []string{"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"} statements := splitSQL(ticket.SQLContent) + // If batch contains ANY DDL, skip EXPLAIN precheck entirely — + // DDL changes the schema so subsequent DML can't be EXPLAINed beforehand + hasDDL := false for _, stmt := range statements { - stmt = strings.TrimSpace(stmt) - if stmt == "" { - continue - } - upperStmt := strings.ToUpper(stmt) - if strings.HasPrefix(upperStmt, "EXPLAIN") { - continue - } - // Skip DDL — EXPLAIN does not work on DDL statements - isDDL := false + upper := strings.ToUpper(strings.TrimSpace(stmt)) for _, prefix := range ddlPrefixes { - if strings.HasPrefix(upperStmt, prefix) { - isDDL = true + if strings.HasPrefix(upper, prefix) { + hasDDL = true break } } - if isDDL { - continue - } - rows, err := db.QueryContext(ctx, "EXPLAIN "+stmt) - if err != nil { - execErr = fmt.Sprintf("预检查失败(EXPLAIN): %v", err) + if hasDDL { break } - rows.Close() + } + if !hasDDL { + for _, stmt := range statements { + stmt = strings.TrimSpace(stmt) + if stmt == "" { + continue + } + upperStmt := strings.ToUpper(stmt) + if strings.HasPrefix(upperStmt, "EXPLAIN") { + continue + } + rows, err := db.QueryContext(ctx, "EXPLAIN "+stmt) + if err != nil { + execErr = fmt.Sprintf("预检查失败(EXPLAIN): %v", err) + break + } + rows.Close() + } } if execErr != "" { failStatus := "failed" @@ -1026,3 +1060,22 @@ func getApplicantName(userID int) string { } return "" } + +// isClusterInstance checks if a Redis instance is configured as cluster mode +func isClusterInstance(instance *model.Instance) bool { + return instance.ClusterMode != nil && *instance.ClusterMode +} + +// parseMovedTarget extracts the target address from a MOVED response +// Input: "MOVED 8480 172.19.0.4:9002" → Output: "172.19.0.4:9002" +func parseMovedTarget(errMsg string) string { + // MOVED : + if !strings.HasPrefix(errMsg, "MOVED ") { + return "" + } + parts := strings.SplitN(errMsg, " ", 3) + if len(parts) == 3 { + return parts[2] + } + return "" +}