Files
rootandClaude Opus 5.5 7974a42301 Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:07:42 +08:00

576 lines
14 KiB
Go
Raw Permalink 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.
package playletcache
import (
"context"
"sort"
"strings"
"sync"
"videoweb/pub/common/log"
"videoweb/pub/models/hotsearchmod"
"videoweb/pub/models/playletcategorymod"
"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
categoryByID map[int64]*CategoryItem
categoryBySlug map[string]*CategoryItem
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
}
// CategoryItem 分类缓存项(含 SEO)
type CategoryItem struct {
Id int64
Name string
Slug string
Desc string
Icon 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)
updateCategoryCache(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 updateCategoryCache(ctx context.Context) {
list, err := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{}, options.Find().
SetProjection(bson.D{
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Slug, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Desc, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
{Key: playletcategorymod.FN.PlayletCategory.Status, Value: 1},
{Key: "seoInfo", Value: 1},
}).
SetSort(bson.D{
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
}))
if err != nil {
log.Warn("playletcache.updateCategoryCache failed", log.E(err))
return
}
temp := make(map[int64]*CategoryItem, len(list))
tempBySlug := make(map[string]*CategoryItem, len(list))
for _, item := range list {
if item == nil || item.Id <= 0 {
continue
}
cached := &CategoryItem{
Id: item.Id,
Name: item.Name,
Slug: item.Slug,
Desc: item.Desc,
Icon: item.Icon,
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.categoryByID = temp
cache.categoryBySlug = 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
}
// 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
}
// GetCategory 按 ID 取分类缓存;不存在返回 nil。
func GetCategory(id int64) *CategoryItem {
if id <= 0 {
return nil
}
cache.lock.RLock()
defer cache.lock.RUnlock()
item := cache.categoryByID[id]
if item == nil {
return nil
}
cp := *item
return &cp
}
// GetEnabledCategory 按 ID 取启用分类;未启用或不存在返回 nil。
func GetEnabledCategory(id int64) *CategoryItem {
item := GetCategory(id)
if item == nil || !item.Status {
return nil
}
return item
}
// GetCategoryBySlug 按 slug 取分类缓存;不存在返回 nil。
func GetCategoryBySlug(slug string) *CategoryItem {
slug = strings.TrimSpace(slug)
if slug == "" {
return nil
}
cache.lock.RLock()
defer cache.lock.RUnlock()
item := cache.categoryBySlug[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
}
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
}
// ListEnabledCategories 返回全部启用分类,按 Rank、Id 倒序。
func ListEnabledCategories() []*CategoryItem {
cache.lock.RLock()
defer cache.lock.RUnlock()
result := make([]*CategoryItem, 0, len(cache.categoryByID))
for _, item := range cache.categoryByID {
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
}
// 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
}
// CategoryNameMap 批量取分类名称(含未启用)。
func CategoryNameMap(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.categoryByID[id]; item != nil {
result[id] = item.Name
}
}
return result
}