package cache import ( "context" "encoding/json" "fmt" "log" "time" "soul-api/internal/database" "soul-api/internal/model" "soul-api/internal/redis" ) const defaultTimeout = 2 * time.Second // KeyBookParts 目录接口缓存 key,后台更新章节/内容时需 Del const KeyBookParts = "soul:book:parts" // KeyAllChapters 全书章节列表,default 与 excludeFixed 两种 func KeyAllChapters(cacheKey string) string { if cacheKey == "excludeFixed" { return "soul:book:all-chapters:excludeFixed" } return "soul:book:all-chapters" } // KeyChaptersByPart 篇章内章节,格式 soul:book:chapters-by-part:{partId} func KeyChaptersByPart(partId string) string { return "soul:book:chapters-by-part:" + partId } // KeyChaptersByPartPattern 用于批量删除 chapters-by-part 缓存 const KeyChaptersByPartPattern = "soul:book:chapters-by-part:*" // KeyBookLatestChapters 最新更新章节 const KeyBookLatestChapters = "soul:book:latest-chapters" // KeyFreeChapterIDs 免费章节 ID 列表(JSON 数组) const KeyFreeChapterIDs = "soul:config:free-chapters" // KeyBookHot 热门章节,格式 soul:book:hot:{limit} func KeyBookHot(limit int) string { return "soul:book:hot:" + fmt.Sprint(limit) } const KeyBookRecommended = "soul:book:recommended" const KeyBookStats = "soul:book:stats" const KeyConfigMiniprogram = "soul:config:miniprogram" const KeyConfigAuditMode = "soul:config:audit-mode" const KeyConfigCore = "soul:config:core" const KeyConfigReadExtras = "soul:config:read-extras" // Get 从 Redis 读取,未配置或失败返回 nil(调用方回退 DB) func Get(ctx context.Context, key string, dest interface{}) bool { client := redis.Client() if client == nil { return false } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() val, err := client.Get(ctx, key).Bytes() if err != nil { return false } if dest != nil && len(val) > 0 { _ = json.Unmarshal(val, dest) } return true } // Set 写入 Redis,失败仅打日志不阻塞 func Set(ctx context.Context, key string, val interface{}, ttl time.Duration) { client := redis.Client() if client == nil { return } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() data, err := json.Marshal(val) if err != nil { log.Printf("cache.Set marshal %s: %v", key, err) return } if err := client.Set(ctx, key, data, ttl).Err(); err != nil { log.Printf("cache.Set %s: %v (非致命)", key, err) } } // Del 删除 key,失败仅打日志 func Del(ctx context.Context, key string) { client := redis.Client() if client == nil { return } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() if err := client.Del(ctx, key).Err(); err != nil { log.Printf("cache.Del %s: %v (非致命)", key, err) } } // DelPattern 按模式删除 key(如 soul:book:chapters-by-part:*),用于批量失效 func DelPattern(ctx context.Context, pattern string) { client := redis.Client() if client == nil { return } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout*2) defer cancel() keys, err := client.Keys(ctx, pattern).Result() if err != nil || len(keys) == 0 { return } if err := client.Del(ctx, keys...).Err(); err != nil { log.Printf("cache.DelPattern %s: %v (非致命)", pattern, err) } } // BookPartsTTL 目录接口缓存 TTL,后台更新时主动 Del,此为兜底时长 const BookPartsTTL = 10 * time.Minute // AllChaptersTTL 全书章节列表 TTL const AllChaptersTTL = 10 * time.Minute // ChaptersByPartTTL 篇章内章节 TTL const ChaptersByPartTTL = 10 * time.Minute // FreeChapterIDsTTL 免费章节配置 TTL const FreeChapterIDsTTL = 5 * time.Minute // InvalidateBookParts 后台更新章节/内容时调用,使目录、章节列表等缓存失效 func InvalidateBookParts() { ctx := context.Background() Del(ctx, KeyBookParts) Del(ctx, KeyAllChapters("default")) Del(ctx, KeyAllChapters("excludeFixed")) Del(ctx, KeyBookLatestChapters) Del(ctx, KeyFreeChapterIDs) DelPattern(ctx, KeyChaptersByPartPattern) } // InvalidateBookCache 使热门、推荐、统计等书籍相关缓存失效(与 InvalidateBookParts 同时调用) func InvalidateBookCache() { ctx := context.Background() Del(ctx, KeyBookRecommended) Del(ctx, KeyBookStats) for _, limit := range []int{3, 10, 20, 50} { Del(ctx, KeyBookHot(limit)) } } // InvalidateConfig 配置变更时调用,使小程序 config 及拆分接口缓存失效 func InvalidateConfig() { ctx := context.Background() Del(ctx, KeyConfigMiniprogram) Del(ctx, KeyConfigAuditMode) Del(ctx, KeyConfigCore) Del(ctx, KeyConfigReadExtras) } // BookRelatedTTL 书籍相关接口 TTL(hot/recommended/stats) const BookRelatedTTL = 5 * time.Minute // ConfigTTL 配置接口 TTL const ConfigTTL = 10 * time.Minute // AuditModeTTL 审核模式 TTL,管理端开关后需较快生效 const AuditModeTTL = 1 * time.Minute // KeyChapterContent 章节正文缓存,格式 soul:chapter:content:{mid},存原始 HTML 字符串 func KeyChapterContent(mid int) string { return "soul:chapter:content:" + fmt.Sprint(mid) } // ChapterContentTTL 章节正文 TTL,后台更新时主动 Del const ChapterContentTTL = 30 * time.Minute // GetString 读取字符串(不经过 JSON,适合大文本 content) func GetString(ctx context.Context, key string) (string, bool) { client := redis.Client() if client == nil { return "", false } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() val, err := client.Get(ctx, key).Result() if err != nil { return "", false } return val, true } // SetString 写入字符串(不经过 JSON,适合大文本 content) func SetString(ctx context.Context, key string, val string, ttl time.Duration) { client := redis.Client() if client == nil { return } if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() if err := client.Set(ctx, key, val, ttl).Err(); err != nil { log.Printf("cache.SetString %s: %v (非致命)", key, err) } } // InvalidateChapterContent 章节内容更新时调用,mid<=0 时忽略 func InvalidateChapterContent(mid int) { if mid <= 0 { return } Del(context.Background(), KeyChapterContent(mid)) } // InvalidateChapterContentByID 按业务 id 使章节内容缓存失效(内部查 mid 后调用 InvalidateChapterContent) func InvalidateChapterContentByID(id string) { if id == "" { return } var mid int if err := database.DB().Model(&model.Chapter{}).Where("id = ?", id).Pluck("mid", &mid).Error; err != nil || mid <= 0 { return } InvalidateChapterContent(mid) }