# 灵感瀑布流 Redis 会话缓存实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现灵感瀑布流双向滚动的 Redis 会话级缓存,支持向左滚动时加载历史数据 **Architecture:** 使用 Redis Hash 存储会话级已展示数据,Key 格式 `inspiration_flow:{star_id}:{session_id}`,TTL 30分钟 **Tech Stack:** Go, github.com/redis/go-redis/v9, gin middleware --- ## 文件结构 ``` backend/pkg/database/ └── redis.go # 修改:添加灵感瀑布流缓存操作函数 backend/services/galleryService/ ├── service/gallery_service.go # 修改:使用 Redis 缓存实现双向滚动 └── cache/inspiration_cache.go # 新建:灵感瀑布流缓存操作(可选) ``` --- ### Task 1: 添加灵感瀑布流缓存操作到 Redis 模块 **Files:** - Modify: `backend/pkg/database/redis.go` - [ ] **Step 1: 添加灵感瀑布流缓存 Key 前缀和结构体** ```go const ( BlacklistKeyPrefix = "blacklist:token:" InspirationFlowKeyPrefix = "inspiration_flow:" ) // InspirationFlowCacheEntry 单个展品缓存数据 type InspirationFlowCacheEntry struct { AssetID int64 `json:"asset_id"` Name string `json:"name"` CoverURL string `json:"cover_url"` LikeCount int32 `json:"like_count"` OwnerNickname string `json:"owner_nickname"` Span int32 `json:"span"` MaterialType string `json:"material_type"` } // InspirationFlowCache 会话缓存结构 type InspirationFlowCache struct { DisplayedIDs []int64 `json:"displayed_ids"` // 已展示ID列表 History map[int64]InspirationFlowCacheEntry `json:"history"` // 历史数据详情 } ``` - [ ] **Step 2: 添加灵感瀑布流缓存操作函数** ```go // InspirationFlowKey 生成灵感瀑布流缓存 Key func InspirationFlowKey(starID int64, sessionID string) string { return fmt.Sprintf("%s%d:%s", InspirationFlowKeyPrefix, starID, sessionID) } // GetInspirationFlowCache 获取灵感瀑布流会话缓存 func GetInspirationFlowCache(ctx context.Context, starID int64, sessionID string) (*InspirationFlowCache, error) { if RedisClient == nil { return nil, fmt.Errorf("redis client is not initialized") } key := InspirationFlowKey(starID, sessionID) data, err := RedisClient.Get(ctx, key).Result() if err == redis.Nil { return &InspirationFlowCache{ DisplayedIDs: []int64{}, History: make(map[int64]InspirationFlowCacheEntry), }, nil } if err != nil { return nil, err } var cache InspirationFlowCache if err := json.Unmarshal([]byte(data), &cache); err != nil { return nil, err } return &cache, nil } // SaveInspirationFlowCache 保存灵感瀑布流会话缓存 func SaveInspirationFlowCache(ctx context.Context, starID int64, sessionID string, cache *InspirationFlowCache, ttl time.Duration) error { if RedisClient == nil { return fmt.Errorf("redis client is not initialized") } key := InspirationFlowKey(starID, sessionID) data, err := json.Marshal(cache) if err != nil { return err } return RedisClient.Set(ctx, key, data, ttl).Err() } // AddToInspirationFlowCache 添加展品到会话缓存 func AddToInspirationFlowCache(ctx context.Context, starID int64, sessionID string, entry InspirationFlowCacheEntry, ttl time.Duration) error { cache, err := GetInspirationFlowCache(ctx, starID, sessionID) if err != nil { return err } // 检查是否已存在 for _, id := range cache.DisplayedIDs { if id == entry.AssetID { return nil // 已存在,跳过 } } // 添加到已展示列表 cache.DisplayedIDs = append(cache.DisplayedIDs, entry.AssetID) // 添加到历史详情 if cache.History == nil { cache.History = make(map[int64]InspirationFlowCacheEntry) } cache.History[entry.AssetID] = entry return SaveInspirationFlowCache(ctx, starID, sessionID, cache, ttl) } // GetHistoryPage 获取历史数据的某一页 func GetHistoryPage(cache *InspirationFlowCache, offset, limit int) []InspirationFlowCacheEntry { if cache == nil || cache.History == nil { return []InspirationFlowCacheEntry{} } // 按展示顺序反向遍历(最新展示的在前面) items := make([]InspirationFlowCacheEntry, 0) for i := len(cache.DisplayedIDs) - 1; i >= 0; i-- { if entry, ok := cache.History[cache.DisplayedIDs[i]]; ok { items = append(items, entry) } } // 分页 start := offset end := offset + limit if start >= len(items) { return []InspirationFlowCacheEntry{} } if end > len(items) { end = len(items) } return items[start:end] } ``` - [ ] **Step 3: 提交代码** ```bash git add backend/pkg/database/redis.go git commit -m "feat: 添加灵感瀑布流 Redis 会话缓存操作 Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 2: 修改 gallery_service.go 实现双向滚动 **Files:** - Modify: `backend/services/galleryService/service/gallery_service.go` - [ ] **Step 1: 添加 database 包导入** 确认已导入 `database` 包(如果没有则添加) - [ ] **Step 2: 修改 GetInspirationFlow 函数实现双向滚动** 修改 `GetInspirationFlow` 函数,在向右滚动时更新缓存,在向左滚动时从缓存读取历史数据: **向右滚动(direction=right)修改后:** ```go // 向右滚动:从仓库随机查询新数据(排除已展示),并更新缓存 if direction == "right" { // 获取缓存的已展示ID列表 var excludeIDs []int64 if sessionID != "" { cache, err := database.GetInspirationFlowCache(ctx, starID, sessionID) if err == nil && cache != nil { excludeIDs = cache.DisplayedIDs } } items, err := s.repo.GetRandomExhibitions(starID, materialType, excludeIDs, int(limit), 0) if err != nil { logger.Logger.Warn("GetInspirationFlow failed", zap.Int64("star_id", starID), zap.String("direction", direction), zap.Error(err), ) return nil, "", err } // 转换为pb并更新缓存 pbItems := make([]*pb.InspirationFlowItem, 0, len(items)) for _, item := range items { pbItems = append(pbItems, &pb.InspirationFlowItem{ AssetId: item.AssetID, Name: item.Name, CoverUrl: item.CoverURL, LikeCount: item.LikeCount, OwnerNickname: item.OwnerNickname, Span: item.Span, MaterialType: item.MaterialType, }) // 更新缓存 if sessionID != "" { cacheEntry := database.InspirationFlowCacheEntry{ AssetID: item.AssetID, Name: item.Name, CoverURL: item.CoverURL, LikeCount: item.LikeCount, OwnerNickname: item.OwnerNickname, Span: item.Span, MaterialType: item.MaterialType, } // 忽略错误,不影响主流程 _ = database.AddToInspirationFlowCache(ctx, starID, sessionID, cacheEntry, 30*time.Minute) } } // 生成新游标 newCursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"limit":%d}`, decodedLimit))) // 检查是否还有更多数据 total, err := s.repo.CountValidExhibitions(starID, materialType) hasMore := int64(len(items)) < total return &pb.InspirationFlowData{ Items: pbItems, Cursor: newCursor, HasMore: hasMore, SessionId: sessionID, }, sessionID, nil } ``` **向左滚动(direction=left)修改后:** ```go // 向左滚动:从缓存的历史数据中分页返回 if direction == "left" { if sessionID == "" { return &pb.InspirationFlowData{ Items: []*pb.InspirationFlowItem{}, Cursor: cursor, HasMore: false, SessionId: sessionID, }, sessionID, nil } // 获取缓存 cache, err := database.GetInspirationFlowCache(ctx, starID, sessionID) if err != nil || cache == nil || len(cache.DisplayedIDs) == 0 { return &pb.InspirationFlowData{ Items: []*pb.InspirationFlowItem{}, Cursor: cursor, HasMore: false, SessionId: sessionID, }, sessionID, nil } // 解析 offset 从 cursor 中 offset := 0 if cursor != "" { decoded, err := base64.StdEncoding.DecodeString(cursor) if err == nil { var cursorData map[string]interface{} if json.Unmarshal(decoded, &cursorData) == nil { if o, ok := cursorData["offset"].(float64); ok { offset = int(o) } } } } // 获取分页数据 historyItems := database.GetHistoryPage(cache, offset, int(limit)) hasMore := offset+len(historyItems) < len(cache.DisplayedIDs) // 转换为 pb pbItems := make([]*pb.InspirationFlowItem, 0, len(historyItems)) for _, item := range historyItems { pbItems = append(pbItems, &pb.InspirationFlowItem{ AssetId: item.AssetID, Name: item.Name, CoverUrl: item.CoverURL, LikeCount: item.LikeCount, OwnerNickname: item.OwnerNickname, Span: item.Span, MaterialType: item.MaterialType, }) } // 生成新游标(包含 offset) newCursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"limit":%d,"offset":%d}`, decodedLimit, offset+len(historyItems)))) return &pb.InspirationFlowData{ Items: pbItems, Cursor: newCursor, HasMore: hasMore, SessionId: sessionID, }, sessionID, nil } ``` - [ ] **Step 3: 添加 time 包导入(如果尚未导入)** - [ ] **Step 4: 提交代码** ```bash git add backend/services/galleryService/service/gallery_service.go git commit -m "feat: 实现灵感瀑布流双向滚动 Redis 缓存 Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 3: 验证构建 - [ ] **Step 1: 运行 go build 验证代码编译** ```bash cd /Users/liulujian/Documents/code/TopFansByGithub/backend && go build ./... ``` Expected: 编译成功,无错误 --- ## 变更记录 | 日期 | 变更内容 | |------|---------| | 2026-05-14 | 初始实现计划 |