init: 初始化 web_hgdjb_server 项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 10:47:32 +08:00
co-authored by Claude Opus 4.7
commit f13d33af4d
707 changed files with 147705 additions and 0 deletions
+499
View File
@@ -0,0 +1,499 @@
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
}
+101
View File
@@ -0,0 +1,101 @@
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.PlayName, 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
}