package configser import ( "context" "regexp" "strconv" "strings" "sync" "videoweb/app/api/proto" "videoweb/app/service/translationser" "videoweb/pub/common/log" "videoweb/pub/common/netutil" "videoweb/pub/common/srvdef" "videoweb/pub/dataReport" "videoweb/pub/models/cfg" "videoweb/pub/models/hotnavmod" "videoweb/pub/models/ipblackmod" "videoweb/pub/models/richtextcontentmod" "videoweb/pub/models/videocreatermod" "github.com/robfig/cron/v3" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) var ( CacheMgr CacheInfo // skdCron *cron.Cron ) // richTextImgSrcRe 匹配富文本HTML里 img 标签的 src 图片地址,兼容单引号、双引号和JSON转义后的双引号 var richTextImgSrcRe = regexp.MustCompile(`(?i)(]*?\s*)src\s*=\s*(\\?"([^"\\]*)\\?"|'([^']*)')`) type CacheInfo struct { Lock sync.RWMutex // 读写锁 Config proto.ConfigInfo // 系统配置缓存 Announcement proto.AnnouncementInfo Advitise proto.AdvitiseData IpBlackMaps map[string]struct{} CreateInfo map[int64]*videocreatermod.VideoCreaterInfo // 作者信息 HotNavs []proto.HotNavItem // 热门导航 AppRichTexts []proto.AppRichTextItemResp // APP富文本内容列表 } type appRichTextCacheItem struct { Id int64 `bson:"_id,omitempty"` Title string `bson:"title"` Content string `bson:"content"` Desc string `bson:"desc"` } func init() { skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务 CacheMgr.Config = proto.ConfigInfo{} CacheMgr.CreateInfo = make(map[int64]*videocreatermod.VideoCreaterInfo) CacheMgr.IpBlackMaps = make(map[string]struct{}) } func Start() { ctx := context.Background() dataReport.StartSDKConfigCron() UpdateConfigInfo(ctx) //UpdateAdvitiseData(ctx) UpdateAdvitiseDataFromJt() UpdateAnnouncementData(ctx) updateIpBlack() updateHotNavCache(ctx) updateAppRichTexts() StartTopicCache() go UpdateVideoRankCache() go UpdatePlayletRankCache() go UpdateAllFavoriteDataCount() _, _ = skdCron.AddFunc("0 */5 * * * ?", func() { UpdateConfigInfo(ctx) //UpdateAdvitiseData(ctx) UpdateAdvitiseDataFromJt() UpdateAnnouncementData(ctx) updateIpBlack() updateHotNavCache(ctx) updateAppRichTexts() }) _, _ = skdCron.AddFunc("* */14 * * * ?", func() { updateCreateCache(ctx) }) // 每10分钟统计各标签关联数据量并刷新首页标签缓存 _, _ = skdCron.AddFunc("0 */10 * * * ?", func() { UpdateAllTagDataCount() }) // 每10分钟统计各内容收藏量(整点 10 分) _, _ = skdCron.AddFunc("0 */10 * * * ?", func() { UpdateAllFavoriteDataCount() }) // 各内容榜单每 10 分钟刷新一次,错开整点避免任务扎堆 _, _ = skdCron.AddFunc("0 1/10 * * * ?", func() { UpdateVideoRankCache() }) _, _ = skdCron.AddFunc("0 5/10 * * * ?", func() { UpdatePlayletRankCache() }) skdCron.Start() } func UpdateConfigInfo(ctx context.Context) { config := proto.ConfigInfo{} filter := bson.M{cfg.FN.SysCfg.Id: 1} sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter) if sysCfg == nil { return } config.NewUrlAddr = sysCfg.NewUrlAddr config.NewUrlAddr1 = sysCfg.NewUrlAddr1 config.NewUrlAddr2 = sysCfg.NewUrlAddr2 config.ForeverUrlAddr = sysCfg.ForeverUrlAddr config.GmailAddr = sysCfg.GmailAddr config.AppAddr = sysCfg.AppAddr config.WeiChart = sysCfg.WeiChart config.QQ = sysCfg.QQ config.TgGroup = sysCfg.TgGroup config.PotaoGroup = sysCfg.PotaoGroup config.GitAddr = sysCfg.GitAddr config.TwitterAddr = sysCfg.TwitterAddr config.Telegram = sysCfg.Telegram config.TelegramAdd = sysCfg.TelegramAdd config.TranslationAddr = sysCfg.TranslationAddr config.ImgCdn = sysCfg.ImgAppCdn config.TsCdn = sysCfg.MediaAppCdn config.ImgAppUsCdn = sysCfg.ImgAppUsCdn config.MediaAppUsCdn = sysCfg.MediaAppUsCdn config.PdfAddr = sysCfg.PdfAddr config.WebTitle = sysCfg.WebTitle config.WebKeyworld = sysCfg.WebKeyworld config.WebDescription = sysCfg.WebDescription config.WebSiteName = sysCfg.WebSiteName config.WebAddr = sysCfg.WebAddr config.DHNewUrlAddr1 = sysCfg.DHNewUrlAddr1 config.DHNewUrlAddr2 = sysCfg.DHNewUrlAddr2 config.DHNewUrlAddr3 = sysCfg.DHNewUrlAddr3 config.UseIngAddress = sysCfg.UseAddress config.FreeVPN = sysCfg.FreeVPN config.DHKeyworld = sysCfg.DHKeyworld config.DHTitle = sysCfg.DHTitle config.DHDescription = sysCfg.DHDescription config.DHSiteName = sysCfg.DHSiteName config.ReportSDKConfig = dataReport.GetSDKDomainConfig() config.ChannelCode = sysCfg.ChannelCode config.EnableH265 = sysCfg.EnableH265 cfg.CacheAppKey(sysCfg.AppKey) config.M3U8DayRateLimitMax = sysCfg.M3U8DayRateLimitMax CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() CacheMgr.Config = config } func GetConfigInfo() proto.ConfigInfo { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.Config } func GetAdvitise() proto.AdvitiseData { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.Advitise } func GetAnnouncement() proto.AnnouncementInfo { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return CacheMgr.Announcement } func GetRandCDN() string { config := GetConfigInfo() return config.TsCdn } func updateHotNavCache(ctx context.Context) { filter := bson.M{hotnavmod.FN.HotNav.Status: true} opts := options.Find() opts.SetSort(bson.D{ {Key: hotnavmod.FN.HotNav.Sort, Value: -1}, {Key: hotnavmod.FN.HotNav.CreatedAt, Value: -1}, }) opts.SetLimit(100) list, err := hotnavmod.FindManyHotNav(ctx, filter, opts) if err != nil { log.Warn("updateHotNavCache.FindManyHotNav", log.E(err)) return } config := GetConfigInfo() tempList := make([]proto.HotNavItem, len(list)) for i, item := range list { tempList[i] = proto.HotNavItem{ Id: item.Id, Title: item.Title, Intro: item.Intro, Url: item.Url, Icon: netutil.MergeURL(config.ImgCdn, item.Icon), } } CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() CacheMgr.HotNavs = tempList } func GetHotNavs() []proto.HotNavItem { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() result := make([]proto.HotNavItem, len(CacheMgr.HotNavs)) copy(result, CacheMgr.HotNavs) return result } // GetFooterInfo 查询APP底部信息 func GetFooterInfo() proto.FooterInfo { config := GetConfigInfo() return proto.FooterInfo{ Brand: buildFooterBrand(config), NavList: buildFooterNavList(), ContactList: buildFooterContactList(config), } } // buildFooterBrand 构建APP底部品牌信息 func buildFooterBrand(config proto.ConfigInfo) proto.FooterBrand { return proto.FooterBrand{ Title: config.WebSiteName, Content: config.WebDescription, } } // buildFooterNavList 构建APP底部导航列表 func buildFooterNavList() []proto.TranslationCategory { bottom := translationser.GetTranslationBottomList() return append([]proto.TranslationCategory{}, bottom.CategoryList...) } // buildFooterContactList 构建APP底部联系方式,固定返回原型要求的四项 func buildFooterContactList(config proto.ConfigInfo) []proto.FooterContact { return []proto.FooterContact{ {Name: "E-mail", Value: config.GmailAddr}, {Name: "商务合作", Value: config.Telegram}, {Name: "官方群", Value: firstNotEmpty(config.TgGroup, config.TelegramAdd)}, {Name: "官方推特", Value: config.TwitterAddr}, } } // firstNotEmpty 返回第一个非空字符串 func firstNotEmpty(values ...string) string { for _, value := range values { value = strings.TrimSpace(value) if value != "" { return value } } return "" } // GetAppRichTextList 查询APP富文本内容列表 func GetAppRichTextList(rawIds string, rawTitles string) proto.AppRichTextListResp { ids := parseInt64Set(rawIds) titles := parseStringSet(rawTitles) data := getCachedAppRichTexts() if len(data) == 0 { updateAppRichTexts() data = getCachedAppRichTexts() } return proto.AppRichTextListResp{List: filterAppRichTexts(data, ids, titles)} } // getCachedAppRichTexts 读取APP富文本缓存副本 func getCachedAppRichTexts() []proto.AppRichTextItemResp { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() return append([]proto.AppRichTextItemResp{}, CacheMgr.AppRichTexts...) } // filterAppRichTexts 按富文本ID或标题过滤缓存数据 func filterAppRichTexts(data []proto.AppRichTextItemResp, ids map[int64]struct{}, titles map[string]struct{}) []proto.AppRichTextItemResp { list := make([]proto.AppRichTextItemResp, 0, len(data)) for _, item := range data { if len(ids) > 0 { if _, ok := ids[item.Id]; !ok { continue } } if len(titles) > 0 { if _, ok := titles[item.Title]; !ok { continue } } list = append(list, item) } return list } // parseInt64Set 解析英文逗号分隔的ID集合 func parseInt64Set(raw string) map[int64]struct{} { raw = strings.TrimSpace(raw) if raw == "" { return nil } resp := make(map[int64]struct{}) for _, item := range strings.Split(raw, ",") { id, err := strconv.ParseInt(strings.TrimSpace(item), 10, 64) if err != nil || id <= 0 { continue } resp[id] = struct{}{} } return resp } // parseStringSet 解析英文逗号分隔的字符串集合 func parseStringSet(raw string) map[string]struct{} { raw = strings.TrimSpace(raw) if raw == "" { return nil } resp := make(map[string]struct{}) for _, item := range strings.Split(raw, ",") { item = strings.TrimSpace(item) if item == "" { continue } resp[item] = struct{}{} } return resp } // updateAppRichTexts 刷新APP富文本内容缓存 func updateAppRichTexts() { resp, err := queryAppRichTexts(context.Background()) if err != nil { log.Warn("查询APP富文本内容失败", log.E(err)) return } CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() CacheMgr.AppRichTexts = resp } // queryAppRichTexts 查询启用状态的APP富文本内容,统一供定时缓存和接口兜底加载使用 func queryAppRichTexts(ctx context.Context) ([]proto.AppRichTextItemResp, *srvdef.SrvErr) { filter := bson.M{ richtextcontentmod.FN.RichTextContentModel.Status: true, } opts := options.Find(). SetProjection(bson.M{ richtextcontentmod.FN.RichTextContentModel.Id: 1, richtextcontentmod.FN.RichTextContentModel.Title: 1, richtextcontentmod.FN.RichTextContentModel.Content: 1, richtextcontentmod.FN.RichTextContentModel.Desc: 1, }). SetSort(bson.D{ {Key: richtextcontentmod.FN.RichTextContentModel.Sort, Value: -1}, {Key: richtextcontentmod.FN.RichTextContentModel.UpdatedAt, Value: -1}, }). SetLimit(100) list := make([]appRichTextCacheItem, 0) err := richtextcontentmod.FindManyRichTextContentModelModelWithBind(ctx, &list, filter, opts) if err != nil { log.Warn("查询APP富文本内容失败", log.Any("filter", filter), log.E(err)) return nil, err } imgCdn := GetConfigInfo().ImgCdn resp := make([]proto.AppRichTextItemResp, 0, len(list)) for _, item := range list { resp = append(resp, proto.AppRichTextItemResp{ Id: item.Id, Title: item.Title, Content: applyRichTextImgSrcCdn(imgCdn, item.Content), Desc: item.Desc, }) } return resp, nil } // applyRichTextImgSrcCdn 给富文本 content 中 img 标签的 src 相对图片地址拼接图片CDN func applyRichTextImgSrcCdn(imgCdn string, content string) string { if imgCdn == "" || content == "" { return content } return richTextImgSrcRe.ReplaceAllStringFunc(content, func(match string) string { parts := richTextImgSrcRe.FindStringSubmatch(match) if len(parts) < 5 { return match } prefix := parts[1] quote := `"` src := parts[3] if src == "" { quote = `'` src = parts[4] } else if strings.HasPrefix(parts[2], `\"`) { quote = `\"` } src = strings.TrimSpace(src) if src == "" || isAbsoluteRichTextSrc(src) { return match } return prefix + "src=" + quote + netutil.MergeURL(imgCdn, src) + quote }) } // isAbsoluteRichTextSrc 判断图片地址是否已经是完整地址或特殊协议,避免重复拼接CDN func isAbsoluteRichTextSrc(src string) bool { lowerSrc := strings.ToLower(src) return strings.HasPrefix(lowerSrc, "http://") || strings.HasPrefix(lowerSrc, "https://") || strings.HasPrefix(lowerSrc, "data:") || strings.HasPrefix(lowerSrc, "blob:") || strings.HasPrefix(src, "//") } func updateIpBlack() { ctx := context.Background() list, err := ipblackmod.FindManyIPBlackModel(ctx, bson.M{}) if err != nil { log.Error("updateIpBlack FindManyIPBlackModel error", log.E(err)) return } tempMaps := make(map[string]struct{}) for _, v := range list { tempMaps[v.Ip] = struct{}{} } CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() CacheMgr.IpBlackMaps = tempMaps } func CheckIpBlack(ip string) bool { CacheMgr.Lock.RLock() defer CacheMgr.Lock.RUnlock() if _, ok := CacheMgr.IpBlackMaps[ip]; ok { return true } return false } func Stop() { dataReport.StopSDKConfigCron() skdCron.Stop() }