Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/adcenter"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func UpdateAnnouncementData(ctx context.Context) {
|
||||
config := GetConfigInfo()
|
||||
resp := proto.AnnouncementInfo{
|
||||
VideoInfo: make([]proto.VideoInfoData, 0),
|
||||
PdfAddr: config.PdfAddr,
|
||||
NewUrlAddr: config.DHNewUrlAddr1,
|
||||
NewUrlAddr1: config.DHNewUrlAddr2,
|
||||
NewUrlAddr2: config.DHNewUrlAddr3,
|
||||
ForeverUrlAddr: config.ForeverUrlAddr,
|
||||
AppAddr: config.AppAddr,
|
||||
TranslationAddr: config.TranslationAddr,
|
||||
}
|
||||
|
||||
videoFilter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
videoinformationmod.FN.VideoInformation.IsHot: true,
|
||||
}
|
||||
videoOpt := &options.FindOptions{}
|
||||
videoOpt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Sort: -1})
|
||||
videoOpt.SetLimit(2)
|
||||
|
||||
videoList, serr := videoinformationmod.FindManyVideoInformations(ctx, videoFilter, videoOpt)
|
||||
if serr != nil {
|
||||
log.Error("VideoCategoryData", log.Any("videoFilter", videoFilter), log.E(serr))
|
||||
return
|
||||
}
|
||||
// 查询视频
|
||||
for _, videoInfo := range videoList {
|
||||
v := proto.VideoInfoToData(videoInfo, config.ImgCdn, GetCreateInfo(videoInfo.CreatorId, config.ImgCdn))
|
||||
for _, tagId := range videoInfo.Tags {
|
||||
tagInfo := GetVideoTag(tagId)
|
||||
if tagInfo != nil {
|
||||
v.Tags = append(v.Tags, proto.TagsInfoData{
|
||||
Id: tagInfo.Id,
|
||||
Name: tagInfo.Name,
|
||||
Slug: tagInfo.Slug,
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.VideoInfo = append(resp.VideoInfo, v)
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Announcement = resp
|
||||
}
|
||||
|
||||
// UpdateAdvitiseDataFromJt 从广告中心拉取福利页广告
|
||||
func UpdateAdvitiseDataFromJt() {
|
||||
//config := GetConfigInfo()
|
||||
var resp proto.AdvitiseData
|
||||
jtAds, err := adcenter.JtAdvertiseThreeServer(appg.Conf.AdCenter.ApiDomain, appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode, appg.Conf.AdCenter.AesKey)
|
||||
if err != nil {
|
||||
log.Error("JtAdvertiseThreeServer error occur", log.E(err))
|
||||
return
|
||||
}
|
||||
for _, _ = range jtAds {
|
||||
//pos, err := strconv.Atoi(loc.AdvertiseLocationCode)
|
||||
//if err != nil {
|
||||
// log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode))
|
||||
// continue
|
||||
//}
|
||||
////if pos < 100001 {
|
||||
//// continue
|
||||
////}
|
||||
//ads := make([]proxy.AdvitiseInfo, 0)
|
||||
//for _, ad := range loc.AdDetailInfoList {
|
||||
// //extra := ad.GetExtraData()
|
||||
// ads = append(ads, proxy.AdvitiseInfo{
|
||||
// Id: ad.AdvertiseCode,
|
||||
// Location: int64(pos),
|
||||
// Title: ad.AdvertiseName,
|
||||
// Images: netutil.MergeURL(config.ImgCdn, ad.GetCoverLsj()),
|
||||
// Link: ad.GetRealLink(),
|
||||
// })
|
||||
//}
|
||||
//
|
||||
//switch pos {
|
||||
//case 1:
|
||||
// resp.HomeList = ads
|
||||
//case 2:
|
||||
// resp.DetailTopList = ads
|
||||
//case 3:
|
||||
// resp.NineGridList = ads
|
||||
//case 4:
|
||||
// resp.DetailbannerList = ads
|
||||
//case 5:
|
||||
// resp.LaunchScreenList = ads
|
||||
//case 6:
|
||||
// resp.MustAppList = ads
|
||||
//case 7:
|
||||
// resp.TitleAppList = ads
|
||||
//case 8:
|
||||
// resp.FriendList = ads
|
||||
//case 9:
|
||||
// resp.DialogList = ads
|
||||
//case 10:
|
||||
// resp.PostList = ads
|
||||
//case 11:
|
||||
// resp.FloatingList = ads
|
||||
//case 12:
|
||||
// resp.AList = ads
|
||||
//case 13:
|
||||
// resp.BList = ads
|
||||
//case 14:
|
||||
// resp.VideoList = ads
|
||||
//}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Advitise = resp
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
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)(<img\b[^>]*?\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()
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ClientIPFromCtx 从 gin 请求上下文解析客户端 IP。
|
||||
func ClientIPFromCtx(ctx context.Context) string {
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok || c == nil {
|
||||
return ""
|
||||
}
|
||||
return ipaddr.ResolveClientIP(c)
|
||||
}
|
||||
|
||||
// IsOverseasIP 与 m3u8 TS 域名判断一致:按 v4/v6 库识别是否海外。
|
||||
func IsOverseasIP(ip string) bool {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip == "" {
|
||||
return false
|
||||
}
|
||||
detector := appg.DetectorV6
|
||||
if ipaddr.IsIPv4(ip) {
|
||||
detector = appg.DetectorV4
|
||||
}
|
||||
if detector == nil {
|
||||
return false
|
||||
}
|
||||
ok, _ := detector.IsOverseasIP(ip)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetAppImageCdn 海外 IP 且配置了 imgAppUsCdn 时用海外图床,否则用 imgAppCdn。
|
||||
func GetAppImageCdn(ip string) string {
|
||||
cfg := GetConfigInfo()
|
||||
if IsOverseasIP(ip) {
|
||||
if us := strings.TrimSpace(cfg.ImgAppUsCdn); us != "" {
|
||||
return us
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(cfg.ImgCdn)
|
||||
}
|
||||
|
||||
// ImageCdnFromCtx 按当前请求 IP 选择图片 CDN。
|
||||
func ImageCdnFromCtx(ctx context.Context) string {
|
||||
return GetAppImageCdn(ClientIPFromCtx(ctx))
|
||||
}
|
||||
|
||||
// GetRequestConfig 返回当前请求应使用的配置副本,海外 IP 且海外图床非空时覆盖 ImgCdn。
|
||||
func GetRequestConfig(ctx context.Context) proto.ConfigInfo {
|
||||
cfg := GetConfigInfo()
|
||||
if cdn := GetAppImageCdn(ClientIPFromCtx(ctx)); cdn != "" {
|
||||
cfg.ImgCdn = cdn
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// RewriteImageCdn 将已拼好的国内图床域名替换为当前请求应使用的图床,用于缓存数据。
|
||||
func RewriteImageCdn(ctx context.Context, data any) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
from := strings.TrimRight(strings.TrimSpace(GetConfigInfo().ImgCdn), "/")
|
||||
to := strings.TrimRight(GetAppImageCdn(ClientIPFromCtx(ctx)), "/")
|
||||
if from == "" || to == "" || from == to {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
replaced := bytes.ReplaceAll(raw, []byte(from), []byte(to))
|
||||
if bytes.Equal(raw, replaced) {
|
||||
return
|
||||
}
|
||||
_ = json.Unmarshal(replaced, data)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// AppContentTagList 查询APP端有绑定内容数量的启用统一标签列表
|
||||
func AppContentTagList(ctx context.Context, req *proto.AppContentTagListReq) (*proto.AppContentTagListResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
req = &proto.AppContentTagListReq{}
|
||||
}
|
||||
filter := bson.M{
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
}
|
||||
bindCountExpr := appContentTagTotalCountExpr()
|
||||
if req.ContentTagType != nil {
|
||||
if !isValidAppContentTagType(*req.ContentTagType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
countField := appContentTagCountField(*req.ContentTagType)
|
||||
filter[contenttagmod.FN.ContentTag.Types] = *req.ContentTagType
|
||||
filter[countField] = bson.M{"$gt": 0}
|
||||
bindCountExpr = bson.M{"$ifNull": bson.A{"$" + countField, 0}}
|
||||
} else {
|
||||
filter["$or"] = []bson.M{
|
||||
{contenttagmod.FN.ContentTag.VideoCount: bson.M{"$gt": 0}},
|
||||
{contenttagmod.FN.ContentTag.PlayletCount: bson.M{"$gt": 0}},
|
||||
}
|
||||
}
|
||||
list := make([]*proto.AppContentTagItem, 0)
|
||||
pipeline := []bson.M{
|
||||
{"$match": filter},
|
||||
{"$addFields": bson.M{"bindCount": bindCountExpr}},
|
||||
{"$sort": bson.D{
|
||||
{Key: "bindCount", Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
}},
|
||||
{"$project": bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: 1,
|
||||
contenttagmod.FN.ContentTag.Name: 1,
|
||||
contenttagmod.FN.ContentTag.Slug: 1,
|
||||
contenttagmod.FN.ContentTag.Types: 1,
|
||||
contenttagmod.FN.ContentTag.Rank: 1,
|
||||
contenttagmod.FN.ContentTag.VideoCount: 1,
|
||||
contenttagmod.FN.ContentTag.PlayletCount: 1,
|
||||
"seoInfo": 1,
|
||||
"bindCount": 1,
|
||||
}},
|
||||
}
|
||||
if err := contenttagmod.AggregateContentTag(ctx, &list, pipeline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && item.SeoInfo.Keyworld == nil {
|
||||
item.SeoInfo.Keyworld = make([]string, 0)
|
||||
}
|
||||
}
|
||||
return &proto.AppContentTagListResp{List: list}, nil
|
||||
}
|
||||
|
||||
// HasEnabledContentTagBind 判断统一标签是否启用、支持当前内容类型且已有绑定内容
|
||||
func HasEnabledContentTagBind(ctx context.Context, tagId int64, tagType contenttagmod.ContentTagType) (bool, *srvdef.SrvErr) {
|
||||
if tagId <= 0 {
|
||||
return false, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
if !isValidAppContentTagType(tagType) {
|
||||
return false, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
countField := appContentTagCountField(tagType)
|
||||
_, err := contenttagmod.FindOneContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: tagId,
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
contenttagmod.FN.ContentTag.Types: tagType,
|
||||
countField: bson.M{"$gt": 0},
|
||||
}, options.FindOne().SetProjection(bson.M{contenttagmod.FN.ContentTag.Id: 1}))
|
||||
if err != nil {
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// appContentTagTotalCountExpr 计算统一标签视频和短剧绑定数量合计,只用于查询排序不落库
|
||||
func appContentTagTotalCountExpr() bson.M {
|
||||
return bson.M{"$add": bson.A{
|
||||
bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.VideoCount, 0}},
|
||||
bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.PlayletCount, 0}},
|
||||
}}
|
||||
}
|
||||
|
||||
// isValidAppContentTagType 判断APP端统一标签类型是否有效
|
||||
func isValidAppContentTagType(tagType contenttagmod.ContentTagType) bool {
|
||||
return tagType == contenttagmod.ContentTagTypeVideo ||
|
||||
tagType == contenttagmod.ContentTagTypePlaylet
|
||||
}
|
||||
|
||||
// appContentTagCountField 返回当前内容类型对应的绑定数量字段
|
||||
func appContentTagCountField(tagType contenttagmod.ContentTagType) string {
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
return contenttagmod.FN.ContentTag.VideoCount
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
return contenttagmod.FN.ContentTag.PlayletCount
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func updateCreateCache(ctx context.Context) {
|
||||
// 更新作者相关缓存
|
||||
createList, err := videocreatermod.FindManyVideoCreaterInfos(ctx, bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Status: true,
|
||||
}, options.Find().SetLimit(200).SetSort(bson.M{videocreatermod.FN.VideoCreaterInfo.Sort: -1}))
|
||||
if err != nil {
|
||||
log.Warn("updateCreateCache FindManyVideoCreaterIinfos", log.E(err))
|
||||
return
|
||||
}
|
||||
tempCreateMaps := make(map[int64]*videocreatermod.VideoCreaterInfo)
|
||||
for _, createInfo := range createList {
|
||||
tempCreateMaps[createInfo.Id] = createInfo
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
CacheMgr.CreateInfo = tempCreateMaps
|
||||
CacheMgr.Lock.Unlock()
|
||||
}
|
||||
|
||||
func GetCreateInfo(createId int64, imageCdn string) proto.PostCreateInfo {
|
||||
if createId <= 0 {
|
||||
return proto.PostCreateInfo{}
|
||||
}
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
v, ok := CacheMgr.CreateInfo[createId]
|
||||
if !ok {
|
||||
return proto.PostCreateInfo{}
|
||||
}
|
||||
return proto.PostCreateInfo{
|
||||
Id: v.Id,
|
||||
Name: v.Name,
|
||||
Slug: v.Slug,
|
||||
Image: netutil.MergeURL(imageCdn, v.Image),
|
||||
Works: v.Works,
|
||||
CreateType: v.CreateType,
|
||||
Desc: v.Desc,
|
||||
Background: v.Background,
|
||||
CreatedAt: v.CreatedAt,
|
||||
SeoInfo: v.SeoInfo,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
)
|
||||
|
||||
// ApplyVideoH265Policy 根据 sys_cfg.enableH265 决定是否向客户端暴露 H265 地址。
|
||||
func ApplyVideoH265Policy(data *proto.VideoInfoData) {
|
||||
if data == nil || GetConfigInfo().EnableH265 {
|
||||
return
|
||||
}
|
||||
proto.StripH265Url(data)
|
||||
}
|
||||
|
||||
// ApplyVideoH265PolicyList 批量处理视频列表。
|
||||
func ApplyVideoH265PolicyList(list []proto.VideoInfoData) []proto.VideoInfoData {
|
||||
if GetConfigInfo().EnableH265 {
|
||||
return list
|
||||
}
|
||||
for i := range list {
|
||||
proto.StripH265Url(&list[i])
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// GetM3U8DayRateLimitMax 返回每 IP 每天最多可播放的不同正片数。
|
||||
// sys_cfg.m3u8DayRateLimitMax <= 0 时使用默认 80。
|
||||
func GetM3U8DayRateLimitMax() int64 {
|
||||
max := GetConfigInfo().M3U8DayRateLimitMax
|
||||
if max <= 0 {
|
||||
return redisconst.M3U8DayRateLimitMax
|
||||
}
|
||||
return max
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
PlayletRankMgr periodRankCache[proto.PlayletRankData]
|
||||
playletRankRefreshLock sync.Mutex
|
||||
playletRankCacheVersion atomic.Int64
|
||||
)
|
||||
|
||||
func UpdatePlayletRankCache() {
|
||||
version := playletcache.RankCacheVersion()
|
||||
ctx := context.Background()
|
||||
imgCdn := GetConfigInfo().ImgCdn
|
||||
PlayletRankMgr.replace(
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodTotal),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodWeek),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodMonth),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodYear),
|
||||
)
|
||||
playletRankCacheVersion.Store(version)
|
||||
}
|
||||
|
||||
func refreshPlayletRankCacheIfEmpty() {
|
||||
if PlayletRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
playletRankRefreshLock.Lock()
|
||||
defer playletRankRefreshLock.Unlock()
|
||||
if PlayletRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
UpdatePlayletRankCache()
|
||||
}
|
||||
|
||||
func GetPlayletRankList(p RankPeriod, limit int) []proto.PlayletRankData {
|
||||
version := playletcache.RankCacheVersion()
|
||||
if version != playletRankCacheVersion.Load() {
|
||||
playletRankRefreshLock.Lock()
|
||||
if version != playletRankCacheVersion.Load() {
|
||||
UpdatePlayletRankCache()
|
||||
}
|
||||
playletRankRefreshLock.Unlock()
|
||||
}
|
||||
return PlayletRankMgr.get(p, limit, refreshPlayletRankCacheIfEmpty)
|
||||
}
|
||||
|
||||
func playletPeriodWatchField(p RankPeriod) string {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return playletmod.FN.Playlet.WeekWatchs
|
||||
case RankPeriodMonth:
|
||||
return playletmod.FN.Playlet.MonthWatchs
|
||||
case RankPeriodYear:
|
||||
return playletmod.FN.Playlet.YearWatchs
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func buildPlayletRankList(ctx context.Context, imgCdn string, p RankPeriod) []proto.PlayletRankData {
|
||||
var ranked playletmod.PlayletList
|
||||
if p == RankPeriodTotal {
|
||||
if err := playletmod.AggregatePlaylet(ctx, &ranked, contentLikeRankPipeline(
|
||||
playletmod.FN.Playlet.Status, playletmod.StatusUp,
|
||||
playletmod.FN.Playlet.CountLike, playletmod.FN.Playlet.VirtualCountLike,
|
||||
playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id,
|
||||
rankLimit,
|
||||
)); err != nil {
|
||||
log.Warn("buildPlayletRankList like rank query failed", log.E(err))
|
||||
ranked = playletmod.PlayletList{}
|
||||
}
|
||||
} else {
|
||||
watchField := playletPeriodWatchField(p)
|
||||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
watchField: bson.M{"$gt": 0},
|
||||
}, contentWatchRankOpts(watchField, playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id, rankLimit))
|
||||
if err != nil {
|
||||
log.Warn("buildPlayletRankList watch rank query failed", log.Any("period", p), log.E(err))
|
||||
ranked = playletmod.PlayletList{}
|
||||
} else {
|
||||
ranked = list
|
||||
}
|
||||
}
|
||||
if int64(len(ranked)) < rankLimit {
|
||||
needFill := rankLimit - int64(len(ranked))
|
||||
excludeIds := make([]int64, 0, len(ranked))
|
||||
for _, v := range ranked {
|
||||
if v != nil {
|
||||
excludeIds = append(excludeIds, v.Id)
|
||||
}
|
||||
}
|
||||
var filled playletmod.PlayletList
|
||||
if err := playletmod.AggregatePlaylet(ctx, &filled, contentRankFillPipeline(
|
||||
playletmod.FN.Playlet.Id, playletmod.FN.Playlet.Status,
|
||||
playletmod.StatusUp, excludeIds, needFill,
|
||||
)); err != nil {
|
||||
log.Warn("buildPlayletRankList fill sample failed", log.Any("period", p), log.E(err))
|
||||
} else {
|
||||
ranked = append(ranked, filled...)
|
||||
}
|
||||
}
|
||||
tagNameMap, themeNameMap := loadPlayletRankNameMaps(ctx, ranked)
|
||||
result := make([]proto.PlayletRankData, 0, len(ranked))
|
||||
for _, item := range ranked {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, playletToRankData(item, imgCdn, tagNameMap, themeNameMap, p))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func playletToRankData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, p RankPeriod) proto.PlayletRankData {
|
||||
return proto.PlayletRankData{
|
||||
PlayletData: proto.PlayletData{
|
||||
Id: item.Id,
|
||||
Status: item.Status,
|
||||
IsRecommend: item.IsRecommend,
|
||||
IsNew: !item.OnlineAt.IsZero() && item.OnlineAt.After(utils.BeginOfWeekByTime(utils.Now())),
|
||||
IsFeatured: item.IsFeatured,
|
||||
IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
|
||||
ShowOriginal: item.ShowOriginal,
|
||||
OnlineAt: item.OnlineAt,
|
||||
MediaCreatedAt: item.MediaCreatedAt,
|
||||
MediaUpdatedAt: item.MediaUpdatedAt,
|
||||
Title: item.Title,
|
||||
Slug: item.Slug,
|
||||
Desc: item.Desc,
|
||||
Anchor: item.Anchor,
|
||||
Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
|
||||
Preview: netutil.MergeURL(imgCdn, strings.TrimSpace(item.Preview)),
|
||||
TagIds: item.TagIds,
|
||||
TagNames: playletRankTagNamesByIDs(item.TagIds, tagNameMap),
|
||||
TagSlugs: playletRankTagSlugsByIDs(item.TagIds, tagNameMap),
|
||||
ThemeIds: item.ThemeIds,
|
||||
ThemeNames: playletNamesByIDs(item.ThemeIds, themeNameMap),
|
||||
ContentType: item.ContentType,
|
||||
PayType: item.PayType,
|
||||
SingleCoins: item.SingleCoins,
|
||||
TotalEpisode: item.TotalEpisode,
|
||||
UpdateStatus: item.UpdateStatus,
|
||||
CountBrowse: item.DisplayCountBrowse(),
|
||||
HeatValue: item.DisplayHeatValue(),
|
||||
CountCollect: item.CountCollect,
|
||||
CountLike: item.CountLike,
|
||||
VirtualCountLike: item.VirtualCountLike,
|
||||
DisplayCountLike: item.CountLike + item.VirtualCountLike,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
SeoInfo: playletRankSeo(item.SeoInfo),
|
||||
},
|
||||
RankScore: periodWatchNum(p, item.WeekWatchs, item.MonthWatchs, item.YearWatchs, item.CountLike+item.VirtualCountLike),
|
||||
}
|
||||
}
|
||||
|
||||
func playletRankSeo(seo pubdef.SeoValue) pubdef.SeoValue {
|
||||
seo.Title = strings.TrimSpace(seo.Title)
|
||||
seo.Description = strings.TrimSpace(seo.Description)
|
||||
if seo.Keyworld == nil {
|
||||
seo.Keyworld = make([]string, 0)
|
||||
return seo
|
||||
}
|
||||
words := make([]string, 0, len(seo.Keyworld))
|
||||
for _, word := range seo.Keyworld {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
seo.Keyworld = words
|
||||
return seo
|
||||
}
|
||||
|
||||
func playletNamesByIDs(ids []int64, nameMap map[int64]string) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if name := nameMap[id]; name != "" {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadPlayletRankNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string) {
|
||||
tagIDs := make([]int64, 0)
|
||||
themeIDs := make([]int64, 0)
|
||||
seenTag := make(map[int64]struct{})
|
||||
seenTheme := make(map[int64]struct{})
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, id := range item.TagIds {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTag[id]; ok {
|
||||
continue
|
||||
}
|
||||
seenTag[id] = struct{}{}
|
||||
tagIDs = append(tagIDs, id)
|
||||
}
|
||||
for _, id := range item.ThemeIds {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTheme[id]; ok {
|
||||
continue
|
||||
}
|
||||
seenTheme[id] = struct{}{}
|
||||
themeIDs = append(themeIDs, id)
|
||||
}
|
||||
}
|
||||
tagNameMap := make(contenttagmod.ContentTagMap, len(tagIDs))
|
||||
if len(tagIDs) > 0 {
|
||||
if tags, err := contenttagmod.GetMapTags(ctx, tagIDs); err != nil {
|
||||
log.Warn("loadPlayletRankNameMaps tags failed", log.E(err))
|
||||
} else {
|
||||
tagNameMap = tags
|
||||
}
|
||||
}
|
||||
themeNameMap := make(map[int64]string, len(themeIDs))
|
||||
if len(themeIDs) > 0 {
|
||||
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIDs},
|
||||
}, options.Find().SetProjection(bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: 1,
|
||||
playletthememod.FN.PlayletTheme.Name: 1,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("loadPlayletRankNameMaps themes failed", log.E(err))
|
||||
} else {
|
||||
for _, theme := range themes {
|
||||
if theme != nil {
|
||||
themeNameMap[theme.Id] = theme.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tagNameMap, themeNameMap
|
||||
}
|
||||
|
||||
func playletRankTagNamesByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if tag := tags[id]; tag != nil && tag.Name != "" {
|
||||
result = append(result, tag.Name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func playletRankTagSlugsByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if tag := tags[id]; tag != nil && tag.Name != "" {
|
||||
result = append(result, tag.Slug)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const rankLimit = 48
|
||||
|
||||
// NormalizeRankPage 榜单分页:页码从 1 起,pageSize 不是 rankLimit 时强制改为 rankLimit。
|
||||
func NormalizeRankPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize != int64(rankLimit) {
|
||||
pageSize = int64(rankLimit)
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
type periodRankCache[T any] struct {
|
||||
Lock sync.RWMutex
|
||||
Total []T
|
||||
Week []T
|
||||
Month []T
|
||||
Year []T
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) hasAny() bool {
|
||||
c.Lock.RLock()
|
||||
defer c.Lock.RUnlock()
|
||||
return len(c.Total) > 0 || len(c.Week) > 0 || len(c.Month) > 0 || len(c.Year) > 0
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) pick(p RankPeriod) []T {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return c.Week
|
||||
case RankPeriodMonth:
|
||||
return c.Month
|
||||
case RankPeriodYear:
|
||||
return c.Year
|
||||
default:
|
||||
return c.Total
|
||||
}
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) replace(total, week, month, year []T) {
|
||||
c.Lock.Lock()
|
||||
c.Total, c.Week, c.Month, c.Year = total, week, month, year
|
||||
c.Lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) get(p RankPeriod, limit int, refreshIfEmpty func()) []T {
|
||||
c.Lock.RLock()
|
||||
list := c.pick(p)
|
||||
empty := len(list) == 0
|
||||
c.Lock.RUnlock()
|
||||
if empty {
|
||||
refreshIfEmpty()
|
||||
c.Lock.RLock()
|
||||
list = c.pick(p)
|
||||
c.Lock.RUnlock()
|
||||
}
|
||||
return sliceLimit(list, limit)
|
||||
}
|
||||
|
||||
func sliceLimit[T any](s []T, limit int) []T {
|
||||
if limit <= 0 || limit >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:limit]
|
||||
}
|
||||
|
||||
// SliceRankPage 对榜单缓存做分页切片
|
||||
func SliceRankPage[T any](all []T, pageNum, pageSize int64) []T {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
return []T{}
|
||||
}
|
||||
start := (pageNum - 1) * pageSize
|
||||
if start >= int64(len(all)) {
|
||||
return []T{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > int64(len(all)) {
|
||||
end = int64(len(all))
|
||||
}
|
||||
return append([]T{}, all[start:end]...)
|
||||
}
|
||||
|
||||
func contentLikeRankPipeline(statusField string, statusVal interface{}, realLikes, fakeLikes, updatedAt, idField string, limit int64) []bson.M {
|
||||
return []bson.M{
|
||||
{"$match": bson.M{statusField: statusVal}},
|
||||
{"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
|
||||
bson.M{"$ifNull": []interface{}{"$" + realLikes, 0}},
|
||||
bson.M{"$ifNull": []interface{}{"$" + fakeLikes, 0}},
|
||||
}}}},
|
||||
{"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
|
||||
{"$sort": bson.D{
|
||||
{Key: "rankScore", Value: -1},
|
||||
{Key: updatedAt, Value: -1},
|
||||
{Key: idField, Value: -1},
|
||||
}},
|
||||
{"$limit": limit},
|
||||
}
|
||||
}
|
||||
|
||||
func contentWatchRankOpts(watchField, updatedAt, idField string, limit int64) *options.FindOptions {
|
||||
return options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: watchField, Value: -1},
|
||||
{Key: updatedAt, Value: -1},
|
||||
{Key: idField, Value: -1},
|
||||
}).
|
||||
SetLimit(limit)
|
||||
}
|
||||
|
||||
func contentRankFillPipeline(idField, statusField string, statusVal interface{}, excludeIds []int64, needFill int64) []bson.M {
|
||||
return []bson.M{
|
||||
{"$match": bson.M{
|
||||
idField: bson.M{"$nin": excludeIds},
|
||||
statusField: statusVal,
|
||||
}},
|
||||
{"$sample": bson.M{"size": needFill}},
|
||||
}
|
||||
}
|
||||
|
||||
func periodWatchNum(p RankPeriod, week, month, year, total int64) int64 {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return week
|
||||
case RankPeriodMonth:
|
||||
return month
|
||||
case RankPeriodYear:
|
||||
return year
|
||||
default:
|
||||
return total
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/videofavoritemod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// favoriteCountResult 收藏聚合结果
|
||||
type favoriteCountResult struct {
|
||||
Id int64 `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
|
||||
// UpdateAllFavoriteDataCount 统计视频收藏量并写回DB
|
||||
func UpdateAllFavoriteDataCount() {
|
||||
updateVideoFavoriteDataCount(context.Background())
|
||||
}
|
||||
|
||||
// buildFavoritePipeline 构建按时间窗口统计收藏量的聚合 pipeline
|
||||
// contentIdField: favorite表中内容ID字段名(如 videoId)
|
||||
// since: 统计起始时间,零值表示统计全量
|
||||
func buildFavoritePipeline(contentIdField string, since time.Time) []bson.M {
|
||||
matchStage := bson.M{}
|
||||
if !since.IsZero() {
|
||||
matchStage["createdAt"] = bson.M{"$gte": since}
|
||||
}
|
||||
pipeline := []bson.M{}
|
||||
if len(matchStage) > 0 {
|
||||
pipeline = append(pipeline, bson.M{"$match": matchStage})
|
||||
}
|
||||
pipeline = append(pipeline,
|
||||
bson.M{"$group": bson.M{
|
||||
"_id": "$" + contentIdField,
|
||||
"count": bson.M{"$sum": 1},
|
||||
}},
|
||||
)
|
||||
return pipeline
|
||||
}
|
||||
|
||||
// updateVideoFavoriteDataCount 统计视频收藏量,更新 Likes/WeekLikes/MonthLikes/YearLikes
|
||||
func updateVideoFavoriteDataCount(ctx context.Context) {
|
||||
now := time.Now()
|
||||
type period struct {
|
||||
field string
|
||||
since time.Time
|
||||
}
|
||||
periods := []period{
|
||||
{videoinformationmod.FN.VideoInformation.Real.Likes, time.Time{}},
|
||||
{videoinformationmod.FN.VideoInformation.Real.WeekLikes, now.AddDate(0, 0, -7)},
|
||||
{videoinformationmod.FN.VideoInformation.Real.MonthLikes, now.AddDate(0, 0, -30)},
|
||||
{videoinformationmod.FN.VideoInformation.Real.YearLikes, now.AddDate(0, 0, -365)},
|
||||
}
|
||||
idField := videofavoritemod.FN.VideoFavorite.VideoID
|
||||
for _, p := range periods {
|
||||
var results []favoriteCountResult
|
||||
pipeline := buildFavoritePipeline(idField, p.since)
|
||||
if serr := videofavoritemod.AggregateVideoFavorite(ctx, &results, pipeline); serr != nil {
|
||||
log.Warn("updateVideoFavoriteDataCount aggregate failed", log.Any("field", p.field))
|
||||
continue
|
||||
}
|
||||
if len(results) == 0 {
|
||||
continue
|
||||
}
|
||||
writes := make([]mongo.WriteModel, 0, len(results))
|
||||
for _, r := range results {
|
||||
writes = append(writes, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: r.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{p.field: r.Count}}),
|
||||
)
|
||||
}
|
||||
if _, serr := videoinformationmod.BulkWriteVideoInformation(ctx, writes); serr != nil {
|
||||
log.Warn("updateVideoFavoriteDataCount bulk update failed", log.Any("field", p.field))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RankPeriod 排行榜时间维度
|
||||
type RankPeriod int
|
||||
|
||||
const (
|
||||
RankPeriodTotal RankPeriod = 0 // 总榜
|
||||
RankPeriodWeek RankPeriod = 1 // 周榜
|
||||
RankPeriodMonth RankPeriod = 2 // 月榜
|
||||
RankPeriodYear RankPeriod = 3 // 年榜
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/cfg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// GetSystemConfig 读取系统配置(MongoDB id=1)。
|
||||
func GetSystemConfig(ctx context.Context) (*cfg.SysCfg, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
cfg.FN.SysCfg.Id: 1,
|
||||
}
|
||||
return cfg.FindOneSysCfg(ctx, filter)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// tagCountResult 聚合统计结果
|
||||
type tagCountResult struct {
|
||||
Id int64 `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
|
||||
// UpdateAllTagDataCount 并发统计视频和短剧各标签的关联数据量并写回统一标签表
|
||||
func UpdateAllTagDataCount() {
|
||||
ctx := context.Background()
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); updateVideoTagDataCount(ctx) }()
|
||||
go func() { defer wg.Done(); updatePlayletTagDataCount(ctx) }()
|
||||
wg.Wait()
|
||||
updateTagCache()
|
||||
}
|
||||
|
||||
// updateVideoTagDataCount 统计视频标签的关联视频数量,更新 content_tag.videoCount 字段
|
||||
func updateVideoTagDataCount(ctx context.Context) {
|
||||
pipeline := []bson.M{
|
||||
{"$match": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
}},
|
||||
{"$unwind": "$" + videoinformationmod.FN.VideoInformation.Tags},
|
||||
{"$group": bson.M{
|
||||
"_id": "$" + videoinformationmod.FN.VideoInformation.Tags,
|
||||
"count": bson.M{"$sum": 1},
|
||||
}},
|
||||
}
|
||||
var results []tagCountResult
|
||||
if err := videoinformationmod.AggregateVideoInformations(ctx, &results, pipeline); err != nil {
|
||||
log.Warn("updateVideoTagDataCount aggregate failed", log.E(err))
|
||||
return
|
||||
}
|
||||
if len(results) == 0 {
|
||||
results = make([]tagCountResult, 0)
|
||||
}
|
||||
syncContentTagDataCount(ctx, contenttagmod.ContentTagTypeVideo, contenttagmod.FN.ContentTag.VideoCount, results, "updateVideoTagDataCount")
|
||||
}
|
||||
|
||||
// updatePlayletTagDataCount 统计短剧标签的关联短剧数量,更新 content_tag.playletCount 字段
|
||||
func updatePlayletTagDataCount(ctx context.Context) {
|
||||
pipeline := []bson.M{
|
||||
{"$match": bson.M{
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}},
|
||||
{"$unwind": "$" + playletmod.FN.Playlet.TagIds},
|
||||
{"$group": bson.M{
|
||||
"_id": "$" + playletmod.FN.Playlet.TagIds,
|
||||
"count": bson.M{"$sum": 1},
|
||||
}},
|
||||
}
|
||||
var results []tagCountResult
|
||||
if err := playletmod.AggregatePlaylet(ctx, &results, pipeline); err != nil {
|
||||
log.Warn("updatePlayletTagDataCount aggregate failed", log.E(err))
|
||||
return
|
||||
}
|
||||
if len(results) == 0 {
|
||||
results = make([]tagCountResult, 0)
|
||||
}
|
||||
syncContentTagDataCount(ctx, contenttagmod.ContentTagTypePlaylet, contenttagmod.FN.ContentTag.PlayletCount, results, "updatePlayletTagDataCount")
|
||||
}
|
||||
|
||||
func syncContentTagDataCount(ctx context.Context, tagType contenttagmod.ContentTagType, countField string, results []tagCountResult, logName string) {
|
||||
if err := contenttagmod.UpdateManyContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Types: tagType,
|
||||
}, bson.M{"$set": bson.M{countField: 0}}); err != nil {
|
||||
log.Warn(logName+" reset failed", log.E(err))
|
||||
return
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return
|
||||
}
|
||||
writes := make([]mongo.WriteModel, 0, len(results))
|
||||
for _, r := range results {
|
||||
writes = append(writes, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: r.Id,
|
||||
contenttagmod.FN.ContentTag.Types: tagType,
|
||||
}).
|
||||
SetUpdate(bson.M{"$set": bson.M{countField: r.Count}}),
|
||||
)
|
||||
}
|
||||
if _, err := contenttagmod.BulkWriteContentTag(ctx, writes); err != nil {
|
||||
log.Warn(logName+" bulk update failed", log.E(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type TopicCache struct {
|
||||
Lock sync.RWMutex // 读写锁
|
||||
|
||||
ActressWorks []proto.ActressInfoData
|
||||
ActressModelMaps map[int64]*actress.Actress
|
||||
|
||||
AllTagMaps map[int64]proto.TagsInfoData
|
||||
VideoTagMaps map[int64]proto.TagsInfoData
|
||||
|
||||
// 首页展示的标签,按当前类型绑定数量降序
|
||||
HomeVideoTagList []proto.TagsInfoData
|
||||
HomePlayletTagList []proto.TagsInfoData
|
||||
}
|
||||
|
||||
var TopicMgr TopicCache
|
||||
|
||||
func StartTopicCache() {
|
||||
TopicMgr = TopicCache{}
|
||||
TopicMgr.Lock = sync.RWMutex{}
|
||||
TopicMgr.ActressWorks = make([]proto.ActressInfoData, 0)
|
||||
TopicMgr.ActressModelMaps = make(map[int64]*actress.Actress)
|
||||
TopicMgr.HomeVideoTagList = make([]proto.TagsInfoData, 0)
|
||||
TopicMgr.HomePlayletTagList = make([]proto.TagsInfoData, 0)
|
||||
TopicMgr.AllTagMaps = make(map[int64]proto.TagsInfoData)
|
||||
TopicMgr.VideoTagMaps = make(map[int64]proto.TagsInfoData)
|
||||
|
||||
updateTagCache()
|
||||
updateCreateCache(context.Background())
|
||||
updateCategoryCache()
|
||||
_, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
|
||||
updateTagCache()
|
||||
updateCreateCache(context.Background())
|
||||
updateCategoryCache()
|
||||
})
|
||||
}
|
||||
func updateTagCache() {
|
||||
updateContentTagCache()
|
||||
}
|
||||
func updateCategoryCache() {
|
||||
updateActressCache()
|
||||
}
|
||||
|
||||
func updateActressCache() {
|
||||
ctx := context.Background()
|
||||
filter := bson.M{actress.FN.Actress.Status: true}
|
||||
config := GetConfigInfo()
|
||||
otps := &options.FindOptions{}
|
||||
otps.SetSort(bson.D{
|
||||
{Key: actress.FN.Actress.HotValue, Value: -1},
|
||||
{Key: actress.FN.Actress.Id, Value: -1},
|
||||
})
|
||||
list, err := actress.FindManyActresss(ctx, filter, otps)
|
||||
if err != nil {
|
||||
log.Warn("updateActressCache", log.E(err))
|
||||
return
|
||||
}
|
||||
tempWorks := make([]proto.ActressInfoData, len(list))
|
||||
tempModel := make(map[int64]*actress.Actress)
|
||||
for i := 0; i < len(list); i++ {
|
||||
info := proto.ActressInfoToData(*list[i], config.ImgCdn)
|
||||
tempWorks[i] = info
|
||||
tempModel[info.Id] = list[i]
|
||||
}
|
||||
sort.SliceStable(tempWorks, func(i, j int) bool {
|
||||
if tempWorks[i].HotValue == tempWorks[j].HotValue {
|
||||
return tempWorks[i].Id > tempWorks[j].Id
|
||||
}
|
||||
return tempWorks[i].HotValue > tempWorks[j].HotValue
|
||||
})
|
||||
TopicMgr.Lock.Lock()
|
||||
defer TopicMgr.Lock.Unlock()
|
||||
TopicMgr.ActressWorks = tempWorks
|
||||
TopicMgr.ActressModelMaps = tempModel
|
||||
}
|
||||
|
||||
func GetActressInfo(actressId int64) *actress.Actress {
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
v, ok := TopicMgr.ActressModelMaps[actressId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
func updateContentTagCache() {
|
||||
ctx := context.Background()
|
||||
filter := bson.M{
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
"$or": []bson.M{
|
||||
{contenttagmod.FN.ContentTag.VideoCount: bson.M{"$gt": 0}},
|
||||
{contenttagmod.FN.ContentTag.PlayletCount: bson.M{"$gt": 0}},
|
||||
},
|
||||
}
|
||||
list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
}).
|
||||
SetProjection(bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: 1,
|
||||
contenttagmod.FN.ContentTag.Name: 1,
|
||||
contenttagmod.FN.ContentTag.Slug: 1,
|
||||
contenttagmod.FN.ContentTag.Types: 1,
|
||||
contenttagmod.FN.ContentTag.Rank: 1,
|
||||
contenttagmod.FN.ContentTag.VideoCount: 1,
|
||||
contenttagmod.FN.ContentTag.PlayletCount: 1,
|
||||
"seoInfo": 1,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("updateContentTagCache", log.E(err))
|
||||
return
|
||||
}
|
||||
allMaps := make(map[int64]proto.TagsInfoData, len(list))
|
||||
videoMaps := make(map[int64]proto.TagsInfoData)
|
||||
videoHomeList := make([]proto.TagsInfoData, 0)
|
||||
playletHomeList := make([]proto.TagsInfoData, 0)
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, tagType := range item.Types {
|
||||
info := contentTagInfoByType(item, tagType)
|
||||
allMaps[item.Id] = info
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
videoMaps[item.Id] = info
|
||||
if info.Works > 0 {
|
||||
videoHomeList = append(videoHomeList, info)
|
||||
}
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
if info.Works > 0 {
|
||||
playletHomeList = append(playletHomeList, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sortHomeContentTagList(videoHomeList)
|
||||
sortHomeContentTagList(playletHomeList)
|
||||
TopicMgr.Lock.Lock()
|
||||
defer TopicMgr.Lock.Unlock()
|
||||
TopicMgr.AllTagMaps = allMaps
|
||||
TopicMgr.VideoTagMaps = videoMaps
|
||||
TopicMgr.HomeVideoTagList = videoHomeList
|
||||
TopicMgr.HomePlayletTagList = playletHomeList
|
||||
}
|
||||
|
||||
func contentTagInfoByType(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) proto.TagsInfoData {
|
||||
return proto.TagsInfoData{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Slug: item.Slug,
|
||||
SeoInfo: item.SeoInfo,
|
||||
Works: contentTagWorksByType(item, tagType),
|
||||
}
|
||||
}
|
||||
|
||||
func contentTagWorksByType(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) int64 {
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
return item.VideoCount
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
return item.PlayletCount
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func sortHomeContentTagList(list []proto.TagsInfoData) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Works == list[j].Works {
|
||||
return list[i].Id > list[j].Id
|
||||
}
|
||||
return list[i].Works > list[j].Works
|
||||
})
|
||||
}
|
||||
|
||||
func GetActressWorks() []proto.ActressInfoData {
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
return TopicMgr.ActressWorks
|
||||
}
|
||||
|
||||
func GetVideoTag(tagId int64) *proto.TagsInfoData {
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
tag, ok := TopicMgr.VideoTagMaps[tagId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &tag
|
||||
}
|
||||
|
||||
// VideoTagNamesAndSlugs 按视频标签ID顺序返回名称和slug
|
||||
func VideoTagNamesAndSlugs(tagIDs []int64) ([]string, []string) {
|
||||
names := make([]string, 0, len(tagIDs))
|
||||
slugs := make([]string, 0, len(tagIDs))
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
for _, id := range tagIDs {
|
||||
if tag, ok := TopicMgr.VideoTagMaps[id]; ok {
|
||||
names = append(names, tag.Name)
|
||||
slugs = append(slugs, tag.Slug)
|
||||
}
|
||||
}
|
||||
return names, slugs
|
||||
}
|
||||
|
||||
func GetHomeVideoTagList() []proto.TagsInfoData {
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
return TopicMgr.HomeVideoTagList
|
||||
}
|
||||
|
||||
func GetHomePlayletTagList() []proto.TagsInfoData {
|
||||
TopicMgr.Lock.RLock()
|
||||
defer TopicMgr.Lock.RUnlock()
|
||||
return TopicMgr.HomePlayletTagList
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
var (
|
||||
VideoRankMgr periodRankCache[proto.ContentData]
|
||||
videoRankRefreshLock sync.Mutex
|
||||
)
|
||||
|
||||
func UpdateVideoRankCache() {
|
||||
ctx := context.Background()
|
||||
imgCdn := GetConfigInfo().ImgCdn
|
||||
VideoRankMgr.replace(
|
||||
buildVideoRankList(ctx, imgCdn, RankPeriodTotal),
|
||||
buildVideoRankList(ctx, imgCdn, RankPeriodWeek),
|
||||
buildVideoRankList(ctx, imgCdn, RankPeriodMonth),
|
||||
buildVideoRankList(ctx, imgCdn, RankPeriodYear),
|
||||
)
|
||||
}
|
||||
|
||||
func refreshVideoRankCacheIfEmpty() {
|
||||
if VideoRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
videoRankRefreshLock.Lock()
|
||||
defer videoRankRefreshLock.Unlock()
|
||||
if VideoRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
UpdateVideoRankCache()
|
||||
}
|
||||
|
||||
func GetVideoRankList(p RankPeriod, limit int) []proto.ContentData {
|
||||
return VideoRankMgr.get(p, limit, refreshVideoRankCacheIfEmpty)
|
||||
}
|
||||
|
||||
func videoPeriodWatchField(p RankPeriod) string {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return videoinformationmod.FN.VideoInformation.Real.WeekWatchs
|
||||
case RankPeriodMonth:
|
||||
return videoinformationmod.FN.VideoInformation.Real.MonthWatchs
|
||||
case RankPeriodYear:
|
||||
return videoinformationmod.FN.VideoInformation.Real.YearWatchs
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func videoToContentData(item *videoinformationmod.VideoInformation, imgCdn string) proto.ContentData {
|
||||
tagNames, tagSlugs := VideoTagNamesAndSlugs(item.Tags)
|
||||
return proto.ContentData{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Slug: item.Slug,
|
||||
AddedTime: item.AddedTime,
|
||||
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
|
||||
Tags: item.Tags,
|
||||
TagsName: tagNames,
|
||||
TagSlugs: tagSlugs,
|
||||
WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
|
||||
LikeNum: item.Real.Likes + item.Fake.Likes,
|
||||
}
|
||||
}
|
||||
|
||||
func videoToRankContentData(item *videoinformationmod.VideoInformation, imgCdn string, p RankPeriod) proto.ContentData {
|
||||
data := videoToContentData(item, imgCdn)
|
||||
data.WatchNum = periodWatchNum(p, item.Real.WeekWatchs, item.Real.MonthWatchs, item.Real.YearWatchs, data.WatchNum)
|
||||
return data
|
||||
}
|
||||
|
||||
func buildVideoRankList(ctx context.Context, imgCdn string, p RankPeriod) []proto.ContentData {
|
||||
var ranked videoinformationmod.VideoInformationList
|
||||
if p == RankPeriodTotal {
|
||||
if err := videoinformationmod.AggregateVideoInformations(ctx, &ranked, contentLikeRankPipeline(
|
||||
videoinformationmod.FN.VideoInformation.Status, pubdef.InformationStatusVerifyOK,
|
||||
videoinformationmod.FN.VideoInformation.Real.Likes, videoinformationmod.FN.VideoInformation.Fake.Likes,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt, videoinformationmod.FN.VideoInformation.Id,
|
||||
rankLimit,
|
||||
)); err != nil {
|
||||
log.Warn("buildVideoRankList like rank query failed", log.E(err))
|
||||
ranked = videoinformationmod.VideoInformationList{}
|
||||
}
|
||||
} else {
|
||||
watchField := videoPeriodWatchField(p)
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
watchField: bson.M{"$gt": 0},
|
||||
}, contentWatchRankOpts(watchField, videoinformationmod.FN.VideoInformation.UpdatedAt, videoinformationmod.FN.VideoInformation.Id, rankLimit))
|
||||
if err != nil {
|
||||
log.Warn("buildVideoRankList watch rank query failed", log.Any("period", p), log.E(err))
|
||||
ranked = videoinformationmod.VideoInformationList{}
|
||||
} else {
|
||||
ranked = list
|
||||
}
|
||||
}
|
||||
if int64(len(ranked)) < rankLimit {
|
||||
needFill := rankLimit - int64(len(ranked))
|
||||
excludeIds := make([]int64, 0, len(ranked))
|
||||
for _, v := range ranked {
|
||||
excludeIds = append(excludeIds, v.Id)
|
||||
}
|
||||
var filled videoinformationmod.VideoInformationList
|
||||
if err := videoinformationmod.AggregateVideoInformations(ctx, &filled, contentRankFillPipeline(
|
||||
videoinformationmod.FN.VideoInformation.Id, videoinformationmod.FN.VideoInformation.Status,
|
||||
pubdef.InformationStatusVerifyOK, excludeIds, needFill,
|
||||
)); err != nil {
|
||||
log.Warn("buildVideoRankList fill sample failed", log.Any("period", p), log.E(err))
|
||||
} else {
|
||||
ranked = append(ranked, filled...)
|
||||
}
|
||||
}
|
||||
result := make([]proto.ContentData, 0, len(ranked))
|
||||
for _, item := range ranked {
|
||||
result = append(result, videoToRankContentData(item, imgCdn, p))
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user