init: web_hgcg_server 初始化提交

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 16:44:55 +08:00
co-authored by Claude Opus 4.7
commit 189556e702
627 changed files with 126228 additions and 0 deletions
+508
View File
@@ -0,0 +1,508 @@
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)
}
// RefreshThemes 在首页缓存重建时读取最新的题材状态和绑定短剧数。
func RefreshThemes(ctx context.Context) {
updateThemeCache(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 返回缓存中最多 limit 个热搜词。
func ListHotSearch(limit int) []HotSearchItem {
cache.lock.RLock()
defer cache.lock.RUnlock()
if len(cache.hotSearch) == 0 {
return make([]HotSearchItem, 0)
}
count := len(cache.hotSearch)
if limit > 0 && limit < count {
count = limit
}
result := make([]HotSearchItem, count)
copy(result, cache.hotSearch[:count])
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 defaultHomeThemeLimit = 8
// ListHomeThemesByRank 返回已上架且有上架短剧的题材,按 Rank、Id 倒序,最多 limit 条。
func ListHomeThemesByRank(limit int) []*ThemeItem {
if limit <= 0 {
limit = defaultHomeThemeLimit
}
cache.lock.RLock()
defer cache.lock.RUnlock()
result := make([]*ThemeItem, 0, limit)
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].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
}
+59
View File
@@ -0,0 +1,59 @@
package playletcache
import "testing"
func TestListHomeThemesByRankRanksEligibleThemes(t *testing.T) {
cache.lock.Lock()
previous := cache.themeByID
cache.themeByID = map[int64]*ThemeItem{}
for id := int64(1); id <= 10; id++ {
cache.themeByID[id] = &ThemeItem{Id: id, Rank: id, Status: true, PlayletCount: 1}
}
cache.themeByID[11] = &ThemeItem{Id: 11, Rank: 100, Status: false, IsRecommend: true, PlayletCount: 1}
cache.themeByID[12] = &ThemeItem{Id: 12, Rank: 100, Status: true, PlayletCount: 0}
cache.themeByID[13] = &ThemeItem{Id: 13, Rank: 100, Status: true, PlayletCount: 0}
cache.lock.Unlock()
t.Cleanup(func() {
cache.lock.Lock()
cache.themeByID = previous
cache.lock.Unlock()
})
got := ListHomeThemesByRank(8)
if len(got) != 8 {
t.Fatalf("got %d themes, want 8", len(got))
}
for index, item := range got {
want := int64(10 - index)
if item.Id != want {
t.Fatalf("item %d has id %d, want %d", index, item.Id, want)
}
}
}
func TestListHotSearchLimitsAndCopies(t *testing.T) {
cache.lock.Lock()
previous := cache.hotSearch
cache.hotSearch = make([]HotSearchItem, 16)
for index := range cache.hotSearch {
cache.hotSearch[index] = HotSearchItem{Keyword: string(rune('A' + index)), Count: int64(16 - index)}
}
cache.lock.Unlock()
t.Cleanup(func() {
cache.lock.Lock()
cache.hotSearch = previous
cache.lock.Unlock()
})
home := ListHotSearch(15)
if len(home) != 15 || home[0].Keyword != "A" || home[14].Keyword != "O" {
t.Fatalf("home hot search = %+v, want first 15 terms", home)
}
if got := ListHotSearch(10); len(got) != 10 {
t.Fatalf("hot-like has %d terms, want 10", len(got))
}
home[0].Keyword = "changed"
if got := ListHotSearch(1)[0].Keyword; got != "A" {
t.Fatalf("cache term changed to %q", got)
}
}
+100
View File
@@ -0,0 +1,100 @@
package playletcache
import (
"context"
"videoweb/pub/common/log"
"videoweb/pub/models/playletmod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const GuessLikeLimit = 15
var guessLikeProjection = bson.D{
{Key: playletmod.FN.Playlet.Id, Value: 1},
{Key: playletmod.FN.Playlet.Status, Value: 1},
{Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
{Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
{Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
{Key: playletmod.FN.Playlet.Banner, Value: 1},
{Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
{Key: playletmod.FN.Playlet.MediaCreatedAt, Value: 1},
{Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
{Key: playletmod.FN.Playlet.Title, Value: 1},
{Key: playletmod.FN.Playlet.Description, Value: 1},
{Key: playletmod.FN.Playlet.Slug, Value: 1},
{Key: playletmod.FN.Playlet.Desc, Value: 1},
{Key: playletmod.FN.Playlet.Anchor, Value: 1},
{Key: playletmod.FN.Playlet.Cover, Value: 1},
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
{Key: playletmod.FN.Playlet.TagIds, Value: 1},
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
{Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
{Key: playletmod.FN.Playlet.OperateType, Value: 1},
{Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
{Key: "seoInfo", Value: 1},
}
func updateGuessLikeCache(ctx context.Context) {
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
playletmod.FN.Playlet.Status: playletmod.StatusUp,
}, options.Find().
SetProjection(guessLikeProjection).
SetSort(bson.D{
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
{Key: playletmod.FN.Playlet.Id, Value: -1},
}).
SetLimit(GuessLikeLimit))
if err != nil {
log.Warn("playletcache.updateGuessLikeCache failed", log.E(err))
return
}
temp := make([]*playletmod.Playlet, 0, len(list))
for _, item := range list {
if cloned := clonePlaylet(item); cloned != nil {
temp = append(temp, cloned)
}
}
cache.lock.Lock()
cache.guessLike = temp
cache.lock.Unlock()
}
// ListGuessLike 返回收藏量最多的上架短剧,最多15部。
func ListGuessLike() []*playletmod.Playlet {
cache.lock.RLock()
defer cache.lock.RUnlock()
if len(cache.guessLike) == 0 {
return make([]*playletmod.Playlet, 0)
}
result := make([]*playletmod.Playlet, 0, len(cache.guessLike))
for _, item := range cache.guessLike {
if cloned := clonePlaylet(item); cloned != nil {
result = append(result, cloned)
}
}
return result
}
func clonePlaylet(item *playletmod.Playlet) *playletmod.Playlet {
if item == nil || item.Id <= 0 {
return nil
}
cp := *item
if item.ThemeIds != nil {
cp.ThemeIds = append([]int64(nil), item.ThemeIds...)
}
if item.TagIds != nil {
cp.TagIds = append([]int64(nil), item.TagIds...)
}
return &cp
}