diff --git a/backend/internal/handler/redis.go b/backend/internal/handler/redis.go
index f931b5e..f4b806f 100644
--- a/backend/internal/handler/redis.go
+++ b/backend/internal/handler/redis.go
@@ -351,6 +351,18 @@ func writeRedisKeyValue(ctx context.Context, client *redisClientWrapper, key str
}
func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, error) {
+ // 防御:非 Redis 实例不能走 Redis 协议。
+ // 若前端误调 /api/v1/redis/:id/*(如共用组件的 MySQL 实例),
+ // 用 Redis 协议连 3306 会收到 MySQL 握手包,报
+ // `redis: invalid reply: "J\x00\x00\x00\n"` 并抛 500。
+ // 这里提前拦截,给出可读错误。
+ if instance.DBType != "redis" {
+ return nil, resp.UserFacing(fmt.Sprintf(
+ "实例「%s」的类型是 %s,不是 redis。Redis 接口只适用于 redis 实例;"+
+ "MySQL/PostgreSQL 请使用 /api/v1/instances/%d/databases 等对应接口",
+ instance.Name, instance.DBType, instance.ID))
+ }
+
password := instance.Password
// Decrypt if encrypted
if strings.HasPrefix(password, "enc:") {
diff --git a/backend/internal/handler/resp/resp.go b/backend/internal/handler/resp/resp.go
index 099371c..5a384fa 100644
--- a/backend/internal/handler/resp/resp.go
+++ b/backend/internal/handler/resp/resp.go
@@ -7,6 +7,7 @@
package resp
import (
+ "errors"
"log"
"net/http"
@@ -54,7 +55,25 @@ func Forbidden(c *gin.Context, detail string) { Fail(c, http.StatusForbidden,
func NotFound(c *gin.Context, detail string) { Fail(c, http.StatusNotFound, detail) }
// InternalErr 500:服务端 log 完整错误,客户端只收通用文案(防 DB/内部错误原文泄漏)
+// UserFacingError 标记「可以展示给用户」的错误。
+//
+// 典型场景:调用方用错了接口/参数不满足前置条件(例如拿 MySQL 实例去调
+// /api/v1/redis/:id/*),这既不是服务端故障也不该泄漏内部细节,
+// 用 400 + 明确原因比 500 + "服务器内部错误" 更有助于定位。
+type UserFacingError struct{ Msg string }
+
+func (e *UserFacingError) Error() string { return e.Msg }
+
+// UserFacing 构造一个可展示给用户的错误
+func UserFacing(msg string) error { return &UserFacingError{Msg: msg} }
+
func InternalErr(c *gin.Context, err error) {
+ var uf *UserFacingError
+ if errors.As(err, &uf) {
+ log.Printf("[resp] bad request on %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
+ c.JSON(http.StatusBadRequest, gin.H{"detail": uf.Msg, "code": "bad_request"})
+ return
+ }
log.Printf("[resp] internal error on %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
c.JSON(http.StatusInternalServerError, gin.H{"detail": "服务器内部错误,请稍后重试", "code": "internal"})
}
diff --git a/frontend/src/components/SchemaSidebar.tsx b/frontend/src/components/SchemaSidebar.tsx
index 0299dfd..3d1f9d0 100644
--- a/frontend/src/components/SchemaSidebar.tsx
+++ b/frontend/src/components/SchemaSidebar.tsx
@@ -93,7 +93,8 @@ export default function SchemaSidebar({
// -- Redis State(浏览逻辑在共享 RedisKeyBrowser,这里只留库选择与详情预览) --
const [redisDb, setRedisDb] = useState(0);
- const redisDbs = useRedisDbs(Number(instanceId));
+ // 仅 Redis 实例才拉库列表,避免 MySQL/Mongo 也打到 /redis/:id/dbs
+ const redisDbs = useRedisDbs(Number(instanceId), isRedis);
const ctxMenu = useContextMenu();
const dialect: SqlDialect = dbType === 'postgresql' ? 'postgresql' : 'mysql';
@@ -407,22 +408,32 @@ export default function SchemaSidebar({