feat(redis): 支持 List 数据倒序显示

- 为索引列增加倒序优先排序,保持默认正序不变
- 倒序读取真实尾部窗口并保留原始 Redis 索引
- 补充前后端回归测试并同步 Wails 绑定

Refs #751
This commit is contained in:
Syngnat
2026-07-29 11:29:14 +08:00
parent 43c89f9c1e
commit 4222bbbd48
6 changed files with 212 additions and 6 deletions

View File

@@ -1043,6 +1043,57 @@ func (a *App) RedisGetValue(config connection.ConnectionConfig, key string) conn
return connection.QueryResult{Success: true, Data: value}
}
// RedisGetListValue gets a list in its requested display order.
func (a *App) RedisGetListValue(config connection.ConnectionConfig, key string, descending bool) connection.QueryResult {
config.Type = "redis"
client, err := a.getRedisClient(config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
value, err := client.GetValue(key)
if err != nil {
logger.Error(err, "RedisGetListValue 获取失败key=%s", key)
return connection.QueryResult{Success: false, Message: err.Error()}
}
if value.Type != "list" {
return connection.QueryResult{Success: false, Message: a.appText("redis.backend.error.argument_invalid_type", map[string]any{"name": "Redis Key"})}
}
values, ok := value.Value.([]string)
if !ok {
return connection.QueryResult{Success: false, Message: a.appText("redis.backend.error.argument_invalid_type", map[string]any{"name": "Redis List"})}
}
if !descending {
return connection.QueryResult{Success: true, Data: value}
}
windowSize := int64(len(values))
needsTailFetch := value.Length > windowSize
if windowSize == 0 && value.Length > 0 {
windowSize = 1000
if value.Length < windowSize {
windowSize = value.Length
}
needsTailFetch = true
}
if needsTailFetch {
values, err = client.GetList(key, value.Length-windowSize, -1)
if err != nil {
logger.Error(err, "RedisGetListValue 获取尾部数据失败key=%s", key)
return connection.QueryResult{Success: false, Message: err.Error()}
}
} else {
values = append([]string(nil), values...)
}
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
values[left], values[right] = values[right], values[left]
}
value.Value = values
return connection.QueryResult{Success: true, Data: value}
}
// RedisSetString sets a string value
func (a *App) RedisSetString(config connection.ConnectionConfig, key, value string, ttl int64) connection.QueryResult {
config.Type = "redis"

View File

@@ -17,6 +17,12 @@ type capturingRedisClient struct {
deletedHashFields []string
removedListKey string
removedListValue string
valueResult *redislib.RedisValue
listResult []string
listKey string
listStart int64
listStop int64
listCalls int
closed int
closeErr error
}
@@ -50,6 +56,9 @@ func (c *capturingRedisClient) RenameKey(oldKey, newKey string) error { return n
func (c *capturingRedisClient) KeyExists(key string) (bool, error) { return false, nil }
func (c *capturingRedisClient) GetValue(key string) (*redislib.RedisValue, error) {
if c.valueResult != nil {
return c.valueResult, nil
}
return &redislib.RedisValue{}, nil
}
@@ -70,7 +79,11 @@ func (c *capturingRedisClient) DeleteHashField(key string, fields ...string) err
}
func (c *capturingRedisClient) GetList(key string, start, stop int64) ([]string, error) {
return nil, nil
c.listKey = key
c.listStart = start
c.listStop = stop
c.listCalls++
return append([]string(nil), c.listResult...), nil
}
func (c *capturingRedisClient) ListPush(key string, values ...string) error { return nil }
@@ -855,6 +868,57 @@ func TestRedisDeleteHashFieldAcceptsStringSlice(t *testing.T) {
}
}
func TestRedisGetListValueDescendingReadsAndReversesTailWindow(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
CloseAllRedisClients()
client := &capturingRedisClient{
valueResult: &redislib.RedisValue{
Type: "list",
TTL: -1,
Value: []string{"item-0", "item-1", "item-2"},
Length: 5,
},
listResult: []string{"item-2", "item-3", "item-4"},
}
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.RedisGetListValue(connection.ConnectionConfig{
Type: "redis",
Host: "redis.local",
Port: 6379,
}, "tasks", true)
if !result.Success {
t.Fatalf("RedisGetListValue returned failure: %+v", result)
}
if client.listCalls != 1 || client.listKey != "tasks" || client.listStart != 2 || client.listStop != -1 {
t.Fatalf("unexpected list tail request: calls=%d key=%q start=%d stop=%d", client.listCalls, client.listKey, client.listStart, client.listStop)
}
value, ok := result.Data.(*redislib.RedisValue)
if !ok {
t.Fatalf("expected *redis.RedisValue result, got %T", result.Data)
}
if !reflect.DeepEqual(value.Value, []string{"item-4", "item-3", "item-2"}) {
t.Fatalf("unexpected descending list values: %#v", value.Value)
}
if value.Length != 5 || value.TTL != -1 {
t.Fatalf("expected list metadata to remain unchanged, got length=%d ttl=%d", value.Length, value.TTL)
}
}
func TestRedisListRemoveDeletesOneValue(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()