🐛 fix(redis): 修正 hash 字段删除参数序列化错误

- 前端统一按数组传递 hash 字段删除参数
- 后端兼容单字符串与数组两种删除入参
- 补充 Redis hash 字段删除回归测试

Fixes #343
This commit is contained in:
Syngnat
2026-04-17 12:45:21 +08:00
parent 384aea132c
commit 22e4299d3e
4 changed files with 149 additions and 12 deletions

View File

@@ -18,8 +18,8 @@ import (
// Redis client cache
var (
redisCache = make(map[string]redis.RedisClient)
redisCacheMu sync.Mutex
redisCache = make(map[string]redis.RedisClient)
redisCacheMu sync.Mutex
newRedisClientFunc = redis.NewRedisClient
)
@@ -539,16 +539,62 @@ func (a *App) RedisKeyExists(config connection.ConnectionConfig, key string) con
return connection.QueryResult{Success: true, Data: map[string]bool{"exists": exists}}
}
func normalizeRedisStringArgs(raw any, argName string) ([]string, error) {
switch v := raw.(type) {
case nil:
return nil, fmt.Errorf("%s 不能为空", argName)
case string:
text := strings.TrimSpace(v)
if text == "" {
return nil, fmt.Errorf("%s 不能为空", argName)
}
return []string{text}, nil
case []string:
items := make([]string, 0, len(v))
for _, item := range v {
text := strings.TrimSpace(item)
if text == "" {
continue
}
items = append(items, text)
}
if len(items) == 0 {
return nil, fmt.Errorf("%s 不能为空", argName)
}
return items, nil
case []interface{}:
items := make([]string, 0, len(v))
for _, item := range v {
text := strings.TrimSpace(fmt.Sprintf("%v", item))
if text == "" || text == "<nil>" {
continue
}
items = append(items, text)
}
if len(items) == 0 {
return nil, fmt.Errorf("%s 不能为空", argName)
}
return items, nil
default:
return nil, fmt.Errorf("%s 类型无效", argName)
}
}
// RedisDeleteHashField deletes fields from a hash
func (a *App) RedisDeleteHashField(config connection.ConnectionConfig, key string, fields []string) connection.QueryResult {
func (a *App) RedisDeleteHashField(config connection.ConnectionConfig, key string, fields any) connection.QueryResult {
config.Type = "redis"
client, err := a.getRedisClient(config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeleteHashField(key, fields...); err != nil {
logger.Error(err, "RedisDeleteHashField 删除失败key=%s fields=%v", key, fields)
normalizedFields, err := normalizeRedisStringArgs(fields, "fields")
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeleteHashField(key, normalizedFields...); err != nil {
logger.Error(err, "RedisDeleteHashField 删除失败key=%s fields=%v", key, normalizedFields)
return connection.QueryResult{Success: false, Message: err.Error()}
}

View File

@@ -9,7 +9,9 @@ import (
)
type capturingRedisClient struct {
connectConfig connection.ConnectionConfig
connectConfig connection.ConnectionConfig
deletedHashKey string
deletedHashFields []string
}
func (c *capturingRedisClient) Connect(config connection.ConnectionConfig) error {
@@ -45,13 +47,21 @@ func (c *capturingRedisClient) GetString(key string) (string, error) { return ""
func (c *capturingRedisClient) SetString(key, value string, ttl int64) error { return nil }
func (c *capturingRedisClient) GetHash(key string) (map[string]string, error) { return map[string]string{}, nil }
func (c *capturingRedisClient) GetHash(key string) (map[string]string, error) {
return map[string]string{}, nil
}
func (c *capturingRedisClient) SetHashField(key, field, value string) error { return nil }
func (c *capturingRedisClient) DeleteHashField(key string, fields ...string) error { return nil }
func (c *capturingRedisClient) DeleteHashField(key string, fields ...string) error {
c.deletedHashKey = key
c.deletedHashFields = append([]string(nil), fields...)
return nil
}
func (c *capturingRedisClient) GetList(key string, start, stop int64) ([]string, error) { return nil, nil }
func (c *capturingRedisClient) GetList(key string, start, stop int64) ([]string, error) {
return nil, nil
}
func (c *capturingRedisClient) ListPush(key string, values ...string) error { return nil }
@@ -83,7 +93,9 @@ func (c *capturingRedisClient) StreamDelete(key string, ids ...string) (int64, e
func (c *capturingRedisClient) ExecuteCommand(args []string) (interface{}, error) { return nil, nil }
func (c *capturingRedisClient) GetServerInfo() (map[string]string, error) { return map[string]string{}, nil }
func (c *capturingRedisClient) GetServerInfo() (map[string]string, error) {
return map[string]string{}, nil
}
func (c *capturingRedisClient) GetDatabases() ([]redislib.RedisDBInfo, error) { return nil, nil }
@@ -109,7 +121,7 @@ func (c *scriptedRedisClient) Connect(config connection.ConnectionConfig) error
func TestRedisConnectResolvesSavedSecretsByConnectionID(t *testing.T) {
testCases := []struct {
name string
name string
savedConfig connection.ConnectionConfig
runtimeConfig connection.ConnectionConfig
assertResolved func(t *testing.T, got connection.ConnectionConfig)
@@ -426,3 +438,75 @@ func TestRedisConnectRetriesLegacyDefaultRootUserWithoutUsernameAfterAuthFailure
t.Fatalf("expected fallback Redis connect attempt to clear legacy root user, got %q", connectCalls[1].User)
}
}
func TestRedisDeleteHashFieldAcceptsSingleStringField(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
CloseAllRedisClients()
client := &capturingRedisClient{}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisDeleteHashField(connection.ConnectionConfig{
Type: "redis",
Host: "redis.local",
Port: 6379,
}, "profile", "nickname")
if !result.Success {
t.Fatalf("RedisDeleteHashField returned failure: %+v", result)
}
if client.deletedHashKey != "profile" {
t.Fatalf("expected hash key profile, got %q", client.deletedHashKey)
}
if len(client.deletedHashFields) != 1 || client.deletedHashFields[0] != "nickname" {
t.Fatalf("expected one deleted hash field nickname, got %v", client.deletedHashFields)
}
}
func TestRedisDeleteHashFieldAcceptsStringSlice(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
CloseAllRedisClients()
client := &capturingRedisClient{}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisDeleteHashField(connection.ConnectionConfig{
Type: "redis",
Host: "redis.local",
Port: 6379,
}, "profile", []string{"nickname", "avatar"})
if !result.Success {
t.Fatalf("RedisDeleteHashField returned failure: %+v", result)
}
if client.deletedHashKey != "profile" {
t.Fatalf("expected hash key profile, got %q", client.deletedHashKey)
}
if len(client.deletedHashFields) != 2 || client.deletedHashFields[0] != "nickname" || client.deletedHashFields[1] != "avatar" {
t.Fatalf("unexpected deleted hash fields: %v", client.deletedHashFields)
}
}