11 KiB
灵感瀑布流 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>"
Task 2: 修改 gallery_service.go 实现双向滚动
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 | 初始实现计划 |