package translationser import ( "context" "fmt" "net/url" "strings" "sync" "videoweb/app/api/proto" "videoweb/pub/common/log" "videoweb/pub/common/netutil" "videoweb/pub/models/cfg" "videoweb/pub/models/translationcategorymod" "videoweb/pub/models/translationmod" "videoweb/pub/models/videocategorymod" "videoweb/pub/pubdef" "github.com/robfig/cron/v3" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) var ( CacheMgr CacheInfo skdCron *cron.Cron categoryCacheFill sync.Mutex ) type CacheInfo struct { Lock sync.RWMutex // 读写锁 TranslationData proto.AppTranslationInfo // 顶部导航缓存 BottomTranslationData proto.AppTranslationInfo // 底部导航缓存 TranslationMap map[int64]proto.TranslationInfo // 视频分类缓存(按 ID 索引) VideoCategoryMap map[int64]*videocategorymod.VideoCategory } func init() { skdCron = cron.New(cron.WithSeconds()) CacheMgr.TranslationData = proto.AppTranslationInfo{} CacheMgr.BottomTranslationData = proto.AppTranslationInfo{} CacheMgr.TranslationMap = make(map[int64]proto.TranslationInfo) CacheMgr.VideoCategoryMap = make(map[int64]*videocategorymod.VideoCategory) } func Start() { ctx := context.Background() UpdateTranslationInfo(ctx) _, _ = skdCron.AddFunc("0 */5 * * * ?", func() { UpdateTranslationInfo(ctx) }) skdCron.Start() } func UpdateTranslationInfo(ctx context.Context) { filter := bson.M{cfg.FN.SysCfg.Id: 1} sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter) if sysCfg == nil { return } translation := proto.AppTranslationInfo{} bottomTranslation := proto.AppTranslationInfo{} wg := &sync.WaitGroup{} wg.Add(3) var translationCategoryList translationcategorymod.TranslationCategoryList var translationList translationmod.TranslationInfoList var videoCategoryList videocategorymod.VideoCategoryList go func() { defer wg.Done() translationCategoryList, _ = translationcategorymod.FindManyTranslationCategorys(ctx, bson.M{translationcategorymod.FN.TranslationCategory.Status: true}, options.Find().SetLimit(1000).SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1}), ) }() go func() { defer wg.Done() translationList, _ = translationmod.FindManyTranslationInfos(ctx, bson.M{translationmod.FN.TranslationInfo.Status: true}, options.Find().SetLimit(1000).SetSort(bson.M{translationmod.FN.TranslationInfo.Rank: -1}), ) }() go func() { defer wg.Done() videoCategoryList, _ = videocategorymod.FindManyVideoCategorys(ctx, bson.M{videocategorymod.FN.VideoCategory.Status: true}, options.Find().SetLimit(500).SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1}), ) }() wg.Wait() // ---- 构建各分类 ID Map(用于缓存 & 导航分组) ---- newVideoCategoryMap := make(map[int64]*videocategorymod.VideoCategory, len(videoCategoryList)) videoCategoryByNavId := make(map[int64][]*videocategorymod.VideoCategory) for _, c := range videoCategoryList { newVideoCategoryMap[c.Id] = c if c.TranslationCategoryId > 0 { videoCategoryByNavId[c.TranslationCategoryId] = append(videoCategoryByNavId[c.TranslationCategoryId], c) } } // ---- 构建导航树 ---- tempTranslationMap := make(map[int64]proto.TranslationInfo) for _, categoryInfo := range translationCategoryList { categoryTemp := proto.TranslationCategory{ ID: categoryInfo.Id, Name: categoryInfo.Name, Icon: netutil.MergeURL(sysCfg.ImgAppCdn, categoryInfo.Icon), TranslationList: make([]proto.TranslationInfo, 0), CategoryType: categoryInfo.CategoryType, UrlValue: categoryInfo.UrlValue, } if categoryInfo.CategoryType == pubdef.TranslationTypeSub { // 固定路径条目优先放在最前面(根据导航分类的 UrlValue 决定属于哪个内容域) categoryTemp.TranslationList = append(categoryTemp.TranslationList, buildFixedNavItems(categoryInfo.UrlValue)..., ) // 手动配置的导航条目 for _, translationInfo := range translationList { if categoryInfo.Id != translationInfo.TransCategoryId { continue } translationTmp := proto.TranslationInfo{ ID: translationInfo.Id, Name: translationInfo.Name, TransCategoryId: translationInfo.TransCategoryId, } if translationInfo.IsFriendAds { // 友链广告,顶部暂不支持 continue } else if translationInfo.HtmlId > 0 { translationTmp.UrlValue = "/richtext?title=" + url.QueryEscape(strings.TrimSpace(translationInfo.Name)) } else if len(translationInfo.Link) > 0 { translationTmp.UrlValue = translationInfo.Link } if translationTmp.UrlValue == "" { continue } tempTranslationMap[translationInfo.Id] = translationTmp categoryTemp.TranslationList = append(categoryTemp.TranslationList, translationTmp) } // 挂载到该导航的视频分类 for _, c := range videoCategoryByNavId[categoryInfo.Id] { if c.DisplayPosition == pubdef.CategoryDisplayPositionHomeOnly { continue } categoryTemp.TranslationList = append(categoryTemp.TranslationList, proto.TranslationInfo{ ID: c.Id, Name: c.Name, DisplayMode: c.DisplayMode, UrlValue: fmt.Sprintf(pubdef.TranslationPath_VideoMore, c.Id), }) } } else if categoryInfo.CategoryType == pubdef.TranslationTypeBottom { for _, translationInfo := range translationList { if categoryInfo.Id != translationInfo.TransCategoryId { continue } translationTmp := proto.TranslationInfo{ ID: translationInfo.Id, Name: translationInfo.Name, TransCategoryId: translationInfo.TransCategoryId, } if translationInfo.IsFriendAds { translationTmp.IsFriendAds = true } else if translationInfo.HtmlId > 0 { translationTmp.UrlValue = "/richtext?title=" + url.QueryEscape(strings.TrimSpace(translationInfo.Name)) } else if len(translationInfo.Link) > 0 { translationTmp.UrlValue = translationInfo.Link } tempTranslationMap[translationInfo.Id] = translationTmp categoryTemp.TranslationList = append(categoryTemp.TranslationList, translationTmp) } bottomTranslation.CategoryList = append(bottomTranslation.CategoryList, categoryTemp) continue } else { continue } translation.CategoryList = append(translation.CategoryList, categoryTemp) } log.Info("UpdateTranslationInfo", log.Any("categoryList", translation.CategoryList)) CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() CacheMgr.TranslationData = translation CacheMgr.BottomTranslationData = bottomTranslation CacheMgr.TranslationMap = tempTranslationMap CacheMgr.VideoCategoryMap = newVideoCategoryMap } // ---- 导航读取 ---- func GetTranslationData() proto.AppTranslationInfo { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.TranslationData } func GetTranslationBottomList() proto.AppTranslationInfo { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.BottomTranslationData } // ---- 分类读取 ---- func categoryCacheEmpty() bool { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return len(CacheMgr.VideoCategoryMap) == 0 } // EnsureCategoryCaches 若任一分内容分类缓存为空则立即补全;常规定时 5 分钟刷新。 func EnsureCategoryCaches(ctx context.Context) { if !categoryCacheEmpty() { return } categoryCacheFill.Lock() defer categoryCacheFill.Unlock() if !categoryCacheEmpty() { return } UpdateTranslationInfo(ctx) } // EnsureVideoCategoryCache 兼容旧调用,等同 EnsureCategoryCaches。 func EnsureVideoCategoryCache(ctx context.Context) { EnsureCategoryCaches(ctx) } // GetVideoCategory 通过视频分类 ID 获取分类信息,不存在返回 nil func GetVideoCategory(id int64) *videocategorymod.VideoCategory { EnsureCategoryCaches(context.Background()) CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.VideoCategoryMap[id] } // buildFixedNavItems 根据导航分类的 UrlValue 返回对应内容域的固定路径条目, // 这些条目始终排在动态分类和手动配置条目之前。 func buildFixedNavItems(urlValue string) []proto.TranslationInfo { switch urlValue { case pubdef.TranslationPath_Video: return []proto.TranslationInfo{ //{Name: pubdef.TranslationName_Video, UrlValue: pubdef.TranslationPath_Video}, //{Name: pubdef.TranslationName_VideoAll, UrlValue: pubdef.TranslationPath_VideoAll}, //{Name: pubdef.TranslationName_VideoRank, UrlValue: pubdef.TranslationPath_VideoRank}, //{Name: pubdef.TranslationName_ActressRank, UrlValue: pubdef.TranslationPath_ActressRank}, } } return nil }