docs: 添加灵感瀑布流 Redis 会话缓存实现计划
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c026e3b8e7
commit
f154555c4c
@ -0,0 +1,346 @@
|
||||
# 灵感瀑布流 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 | 初始实现计划 |
|
||||
Loading…
Reference in New Issue
Block a user