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

346 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 灵感瀑布流 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 <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修改后**
```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 <noreply@anthropic.com>"
```
---
### Task 3: 验证构建
- [ ] **Step 1: 运行 go build 验证代码编译**
```bash
cd /Users/liulujian/Documents/code/TopFansByGithub/backend && go build ./...
```
Expected: 编译成功,无错误
---
## 变更记录
| 日期 | 变更内容 |
|------|---------|
| 2026-05-14 | 初始实现计划 |