Initial commit: web_hghl_server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 16:28:14 +08:00
co-authored by Claude Opus 4.7
commit 65fca60622
653 changed files with 135901 additions and 0 deletions
+126
View File
@@ -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
}
+454
View File
@@ -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()
}
+85
View File
@@ -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)
}
+116
View File
@@ -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 ""
}
}
+54
View File
@@ -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,
}
}
+35
View File
@@ -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
}
+280
View File
@@ -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
}
+144
View File
@@ -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
}
}
+91
View File
@@ -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 // 年榜
)
+17
View File
@@ -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)
}
+102
View File
@@ -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))
}
}
+233
View File
@@ -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
}
+128
View File
@@ -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
}