topfans/docs/superpowers/plans/2026-05-14-inspiration-flow-cache-implementation-plan.md
zerosaturation f154555c4c docs: 添加灵感瀑布流 Redis 会话缓存实现计划
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 18:15:27 +08:00

11 KiB
Raw Blame History

灵感瀑布流 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 前缀和结构体

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: 添加灵感瀑布流缓存操作函数
// 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: 提交代码
git add backend/pkg/database/redis.go
git commit -m "feat: 添加灵感瀑布流 Redis 会话缓存操作

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Files:

  • Modify: backend/services/galleryService/service/gallery_service.go

  • Step 1: 添加 database 包导入

确认已导入 database 包(如果没有则添加)

  • Step 2: 修改 GetInspirationFlow 函数实现双向滚动

修改 GetInspirationFlow 函数,在向右滚动时更新缓存,在向左滚动时从缓存读取历史数据:

向右滚动direction=right修改后

// 向右滚动:从仓库随机查询新数据(排除已展示),并更新缓存
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修改后

// 向左滚动:从缓存的历史数据中分页返回
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: 提交代码

git add backend/services/galleryService/service/gallery_service.go
git commit -m "feat: 实现灵感瀑布流双向滚动 Redis 缓存

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 3: 验证构建

  • Step 1: 运行 go build 验证代码编译
cd /Users/liulujian/Documents/code/TopFansByGithub/backend && go build ./...

Expected: 编译成功,无错误


变更记录

日期 变更内容
2026-05-14 初始实现计划