package playletcache import ( "context" "sort" "strings" "sync" "videoweb/pub/common/log" "videoweb/pub/models/hotsearchmod" "videoweb/pub/models/playletmod" "videoweb/pub/models/playlettagmod" "videoweb/pub/models/playletthememod" "videoweb/pub/pubdef" "github.com/robfig/cron/v3" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) var ( skdCron *cron.Cron cache struct { lock sync.RWMutex themeByID map[int64]*ThemeItem themeBySlug map[string]*ThemeItem tagByID map[int64]*TagItem tagBySlug map[string]*TagItem hotSearch []HotSearchItem guessLike []*playletmod.Playlet } ) // HotSearchItem 热搜词缓存项 type HotSearchItem struct { Keyword string Count int64 } // ThemeItem 题材缓存项(含 SEO) type ThemeItem struct { Id int64 Name string Slug string Icon string Rank int64 Status bool IsRecommend bool PlayletCount int64 SeoInfo pubdef.SeoValue } // TagItem 标签缓存项(含 SEO) type TagItem struct { Id int64 Name string Slug string Rank int64 Status bool SeoInfo pubdef.SeoValue } // Start 启动题材/标签/热搜词/猜你喜欢缓存,每 10 分钟刷新一次。 func Start() { skdCron = cron.New(cron.WithSeconds()) UpdateCache(context.Background()) _, _ = skdCron.AddFunc("0 */10 * * * ?", func() { UpdateCache(context.Background()) }) skdCron.Start() } // UpdateCache 从 Mongo 全量刷新题材、标签、热搜词和猜你喜欢缓存。 func UpdateCache(ctx context.Context) { updateThemeCache(ctx) updateTagCache(ctx) updateHotSearchCache(ctx) updateGuessLikeCache(ctx) } func updateThemeCache(ctx context.Context) { list, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{}, options.Find(). SetProjection(bson.D{ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1}, {Key: playletthememod.FN.PlayletTheme.Name, Value: 1}, {Key: playletthememod.FN.PlayletTheme.Slug, Value: 1}, {Key: playletthememod.FN.PlayletTheme.Icon, Value: 1}, {Key: playletthememod.FN.PlayletTheme.Rank, Value: 1}, {Key: playletthememod.FN.PlayletTheme.Status, Value: 1}, {Key: playletthememod.FN.PlayletTheme.IsRecommend, Value: 1}, {Key: "seoInfo", Value: 1}, }). SetSort(bson.D{ {Key: playletthememod.FN.PlayletTheme.Rank, Value: -1}, {Key: playletthememod.FN.PlayletTheme.Id, Value: -1}, })) if err != nil { log.Warn("playletcache.updateThemeCache failed", log.E(err)) return } countMap := countUpPlayletsByTheme(ctx) temp := make(map[int64]*ThemeItem, len(list)) tempBySlug := make(map[string]*ThemeItem, len(list)) for _, item := range list { if item == nil || item.Id <= 0 { continue } cached := &ThemeItem{ Id: item.Id, Name: item.Name, Slug: item.Slug, Icon: item.Icon, Rank: item.Rank, Status: item.Status, IsRecommend: item.IsRecommend, PlayletCount: countMap[item.Id], SeoInfo: item.SeoInfo, } temp[item.Id] = cached if slug := strings.TrimSpace(item.Slug); slug != "" { tempBySlug[slug] = cached } } cache.lock.Lock() cache.themeByID = temp cache.themeBySlug = tempBySlug cache.lock.Unlock() } type themePlayletCount struct { Id int64 `bson:"_id"` Count int64 `bson:"count"` } // countUpPlayletsByTheme 按题材统计已上架短剧数量。 func countUpPlayletsByTheme(ctx context.Context) map[int64]int64 { result := make(map[int64]int64) list := make([]themePlayletCount, 0) err := playletmod.AggregatePlaylet(ctx, &list, []bson.M{ { "$match": bson.M{ playletmod.FN.Playlet.Status: playletmod.StatusUp, }, }, { "$unwind": "$" + playletmod.FN.Playlet.ThemeIds, }, { "$group": bson.M{ "_id": "$" + playletmod.FN.Playlet.ThemeIds, "count": bson.M{"$sum": 1}, }, }, }) if err != nil { log.Warn("playletcache.countUpPlayletsByTheme failed", log.E(err)) return result } for _, item := range list { if item.Id <= 0 || item.Count <= 0 { continue } result[item.Id] = item.Count } return result } func updateTagCache(ctx context.Context) { list, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{}, options.Find(). SetProjection(bson.D{ {Key: playlettagmod.FN.PlayletTag.Id, Value: 1}, {Key: playlettagmod.FN.PlayletTag.Name, Value: 1}, {Key: playlettagmod.FN.PlayletTag.Slug, Value: 1}, {Key: playlettagmod.FN.PlayletTag.Rank, Value: 1}, {Key: playlettagmod.FN.PlayletTag.Status, Value: 1}, {Key: "seoInfo", Value: 1}, }). SetSort(bson.D{ {Key: playlettagmod.FN.PlayletTag.Rank, Value: -1}, {Key: playlettagmod.FN.PlayletTag.Id, Value: -1}, })) if err != nil { log.Warn("playletcache.updateTagCache failed", log.E(err)) return } temp := make(map[int64]*TagItem, len(list)) tempBySlug := make(map[string]*TagItem, len(list)) for _, item := range list { if item == nil || item.Id <= 0 { continue } cached := &TagItem{ Id: item.Id, Name: item.Name, Slug: item.Slug, Rank: item.Rank, Status: item.Status, SeoInfo: item.SeoInfo, } temp[item.Id] = cached if slug := strings.TrimSpace(item.Slug); slug != "" { tempBySlug[slug] = cached } } cache.lock.Lock() cache.tagByID = temp cache.tagBySlug = tempBySlug cache.lock.Unlock() } func updateHotSearchCache(ctx context.Context) { list, err := hotsearchmod.FindTopHotSearch(ctx, hotsearchmod.TopLimit) if err != nil { log.Warn("playletcache.updateHotSearchCache failed", log.E(err)) return } temp := make([]HotSearchItem, 0, len(list)) for _, item := range list { if item == nil || strings.TrimSpace(item.Keyword) == "" { continue } temp = append(temp, HotSearchItem{ Keyword: item.Keyword, Count: item.Count, }) } cache.lock.Lock() cache.hotSearch = temp cache.lock.Unlock() } // ListHotSearch 返回缓存的前10个热搜词。 func ListHotSearch() []HotSearchItem { cache.lock.RLock() defer cache.lock.RUnlock() if len(cache.hotSearch) == 0 { return make([]HotSearchItem, 0) } result := make([]HotSearchItem, len(cache.hotSearch)) copy(result, cache.hotSearch) return result } // GetTheme 按 ID 取题材缓存;不存在返回 nil。 func GetTheme(id int64) *ThemeItem { if id <= 0 { return nil } cache.lock.RLock() defer cache.lock.RUnlock() item := cache.themeByID[id] if item == nil { return nil } cp := *item return &cp } // GetEnabledTheme 按 ID 取上架题材;未上架或不存在返回 nil。 func GetEnabledTheme(id int64) *ThemeItem { item := GetTheme(id) if item == nil || !item.Status { return nil } return item } // GetThemeBySlug 按 slug 取题材缓存;不存在返回 nil。 func GetThemeBySlug(slug string) *ThemeItem { slug = strings.TrimSpace(slug) if slug == "" { return nil } cache.lock.RLock() defer cache.lock.RUnlock() item := cache.themeBySlug[slug] if item == nil { return nil } cp := *item return &cp } // GetTag 按 ID 取标签缓存;不存在返回 nil。 func GetTag(id int64) *TagItem { if id <= 0 { return nil } cache.lock.RLock() defer cache.lock.RUnlock() item := cache.tagByID[id] if item == nil { return nil } cp := *item return &cp } // GetEnabledTag 按 ID 取上架标签;未上架或不存在返回 nil。 func GetEnabledTag(id int64) *TagItem { item := GetTag(id) if item == nil || !item.Status { return nil } return item } // ListEnabledTags 返回已上架标签列表,按缓存排序。 func ListEnabledTags() []*TagItem { cache.lock.RLock() defer cache.lock.RUnlock() result := make([]*TagItem, 0, len(cache.tagByID)) seen := make(map[int64]struct{}, len(cache.tagByID)) for _, item := range cache.tagBySlug { if item == nil || !item.Status || item.Id <= 0 { continue } if _, ok := seen[item.Id]; ok { continue } cp := *item result = append(result, &cp) seen[item.Id] = struct{}{} } for _, item := range cache.tagByID { if item == nil || !item.Status || item.Id <= 0 { continue } if _, ok := seen[item.Id]; ok { continue } cp := *item result = append(result, &cp) seen[item.Id] = struct{}{} } sort.SliceStable(result, func(i, j int) bool { if result[i].Rank == result[j].Rank { return result[i].Id > result[j].Id } return result[i].Rank > result[j].Rank }) return result } // GetTagBySlug 按 slug 取标签缓存;不存在返回 nil。 func GetTagBySlug(slug string) *TagItem { slug = strings.TrimSpace(slug) if slug == "" { return nil } cache.lock.RLock() defer cache.lock.RUnlock() item := cache.tagBySlug[slug] if item == nil { return nil } cp := *item return &cp } // ListEnabledThemes 返回全部上架题材,按 Rank、Id 倒序。 func ListEnabledThemes() []*ThemeItem { cache.lock.RLock() defer cache.lock.RUnlock() result := make([]*ThemeItem, 0, len(cache.themeByID)) for _, item := range cache.themeByID { if item == nil || !item.Status { continue } cp := *item result = append(result, &cp) } sort.SliceStable(result, func(i, j int) bool { if result[i].Rank != result[j].Rank { return result[i].Rank > result[j].Rank } return result[i].Id > result[j].Id }) return result } // ListEnabledThemesByPlayletCount 返回有上架短剧的启用题材,按绑定短剧数量倒序 func ListEnabledThemesByPlayletCount(limit int) []*ThemeItem { cache.lock.RLock() defer cache.lock.RUnlock() result := make([]*ThemeItem, 0, len(cache.themeByID)) for _, item := range cache.themeByID { if item == nil || !item.Status || item.PlayletCount <= 0 { continue } cp := *item result = append(result, &cp) } sort.SliceStable(result, func(i, j int) bool { if result[i].PlayletCount != result[j].PlayletCount { return result[i].PlayletCount > result[j].PlayletCount } if result[i].Rank != result[j].Rank { return result[i].Rank > result[j].Rank } return result[i].Id > result[j].Id }) if limit > 0 && len(result) > limit { return result[:limit] } return result } const homeRecommendThemeLimit = 4 // ListRecommendedThemes 返回已上架且推荐的题材,按 Rank、Id 倒序,最多 limit 条。 func ListRecommendedThemes(limit int) []*ThemeItem { if limit <= 0 { limit = homeRecommendThemeLimit } cache.lock.RLock() defer cache.lock.RUnlock() result := make([]*ThemeItem, 0, limit) for _, item := range cache.themeByID { if item == nil || !item.Status || !item.IsRecommend { continue } cp := *item result = append(result, &cp) } sort.SliceStable(result, func(i, j int) bool { if result[i].Rank != result[j].Rank { return result[i].Rank > result[j].Rank } return result[i].Id > result[j].Id }) if len(result) > limit { result = result[:limit] } return result } // ThemeNameMap 批量取题材名称(含未上架,便于列表展示历史题材名)。 func ThemeNameMap(ids []int64) map[int64]string { result := make(map[int64]string, len(ids)) if len(ids) == 0 { return result } cache.lock.RLock() defer cache.lock.RUnlock() for _, id := range ids { if item := cache.themeByID[id]; item != nil { result[id] = item.Name } } return result } // TagNameMap 批量取标签名称(含未上架)。 func TagNameMap(ids []int64) map[int64]string { result := make(map[int64]string, len(ids)) if len(ids) == 0 { return result } cache.lock.RLock() defer cache.lock.RUnlock() for _, id := range ids { if item := cache.tagByID[id]; item != nil { result[id] = item.Name } } return result } // ThemeSlugMap 批量取题材 slug(含未上架)。 func ThemeSlugMap(ids []int64) map[int64]string { result := make(map[int64]string, len(ids)) if len(ids) == 0 { return result } cache.lock.RLock() defer cache.lock.RUnlock() for _, id := range ids { if item := cache.themeByID[id]; item != nil { result[id] = item.Slug } } return result } // TagSlugMap 批量取标签 slug(含未上架)。 func TagSlugMap(ids []int64) map[int64]string { result := make(map[int64]string, len(ids)) if len(ids) == 0 { return result } cache.lock.RLock() defer cache.lock.RUnlock() for _, id := range ids { if item := cache.tagByID[id]; item != nil { result[id] = item.Slug } } return result }