Files
huangguo_server/app/service/recommser/recommser.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

424 lines
12 KiB
Go

package recommser
import (
"91porn-server/common/redis"
"91porn-server/models/v/recmdtag"
"context"
"encoding/json"
"errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"math"
"math/rand"
"strconv"
"sync"
"time"
"91porn-server/app/appg"
"91porn-server/app/proto"
"91porn-server/app/service/adser"
"91porn-server/app/service/shortrecommendser"
"91porn-server/app/service/vidhelpser"
"91porn-server/common"
"91porn-server/common/constant/redisconst"
"91porn-server/common/log"
"91porn-server/common/stderr"
"91porn-server/models/commod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/recommod"
"91porn-server/models/v/tagmod"
"91porn-server/models/v/userrecomod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// GetVidList 获取推荐视频列表
func GetVidList(uid, size uint64) (code stderr.Code, data recommod.VideoListResp) {
// 无客户端请求ID时不启用业务幂等缓存;该入口保留给内部兼容调用。
return GetVidListContext(context.Background(), uid, size, "")
}
// GetVidListContext 获取推荐视频列表,并将请求取消、超时和幂等ID传递到环形队列。
func GetVidListContext(
ctx context.Context,
uid, size uint64,
requestID string,
) (code stderr.Code, data recommod.VideoListResp) {
if ctx == nil {
ctx = context.Background()
}
if size == 0 {
return stderr.Success, data
}
if !appg.Conf.ShortRecommendEnabled() {
return getRandomVidList(uid, size)
}
fetchCtx, cancel := context.WithTimeout(ctx, appg.Conf.ShortRecommendRequestTimeout())
defer cancel()
result, err := shortrecommendser.FetchScoped(
fetchCtx, uid, int(size), requestID, "recommend-list",
)
if err != nil && result.QueueVersion == "" {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
log.Warn("short recommend fetch canceled",
log.E(err), log.Any("uid", uid))
return stderr.Success, data
}
log.Error("short recommend fetch failed, fallback random set",
log.E(err), log.Any("uid", uid))
return getRandomVidList(uid, size)
}
if result.QueueVersion == "" {
return getRandomVidList(uid, size)
}
if err != nil && len(result.Videos) == 0 {
log.Error("short recommend fetch failed after queue reservation",
log.E(err),
log.Any("uid", uid),
log.Any("queueVersion", result.QueueVersion),
log.Any("scanned", result.Scanned),
log.Any("batches", result.Batches))
return stderr.ErrDbQueryError, data
}
if err != nil || result.BudgetExceeded || result.Filtered > 0 {
log.Warn("short recommend fetch completed with filtering",
log.E(err),
log.Any("uid", uid),
log.Any("queueVersion", result.QueueVersion),
log.Any("queueLength", result.QueueLength),
log.Any("requested", size),
log.Any("returned", len(result.Videos)),
log.Any("scanned", result.Scanned),
log.Any("filtered", result.Filtered),
log.Any("batches", result.Batches),
log.Any("budgetExceeded", result.BudgetExceeded))
}
data.QueueVersion = result.QueueVersion
data.TotalPages = int(math.Ceil(float64(result.QueueLength) / float64(size)))
data.HasNext = result.QueueLength > 0
data.VInfos = vidhelpser.EncodeVideoInfoForRecommend(uid, result.Videos)
return stderr.Success, data
}
func getRandomVidList(uid, size uint64) (code stderr.Code, data recommod.VideoListResp) {
totalSize, err := appg.Redis.SCard(redisconst.ShortVideosKey)
if err != nil {
log.Error("GetVidList SCard err", log.E(err))
return stderr.Success, data
}
if totalSize == 0 {
return stderr.Success, data
}
if totalSize <= int64(size) {
data.TotalPages = 1
} else {
data.TotalPages = int(math.Ceil(float64(totalSize) / float64(size)))
}
sampleSize := int64(size * 5)
if sampleSize > totalSize {
sampleSize = totalSize
}
pushVid, err := redis.Handler.SRandMemberN(redisconst.ShortVideosKey, sampleSize)
if err != nil {
return stderr.Success, data
}
voidList := common.String2ObjectID(pushVid)
videos, err := vidmod.GetVideosByIDs(voidList)
if err != nil {
return stderr.Success, data
}
excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true)
if err != nil {
log.Error("GetVidList excluded modules err", log.E(err))
return stderr.Success, data
}
excludedSet := make(map[string]struct{}, len(excluded))
for _, moduleID := range excluded {
excludedSet[moduleID] = struct{}{}
}
allowedIDs := make([]primitive.ObjectID, 0, size)
for _, video := range videos {
if video == nil {
continue
}
if _, blocked := excludedSet[video.MID]; blocked {
continue
}
allowedIDs = append(allowedIDs, video.ID)
if uint64(len(allowedIDs)) == size {
break
}
}
data.VInfos = vidhelpser.GetVideosByIDs(uid, allowedIDs)
data.HasNext = totalSize > 0
return stderr.Success, data
}
// 获取视频广告
func GetAd(uid uint64) []*vidmod.VideoInfo {
adVids := make([]*vidmod.VideoInfo, 0)
key := redisconst.VidAdKey
if err := appg.Redis.GetObj(&adVids, key); err != nil {
log.Warn("GetAd getAdNews Redis GetObj err", log.Any("key", key), log.E(err))
}
if len(adVids) > 0 {
return adVids
}
adnews := vidmod.GetAdVideoIdsList(100)
log.Info("GetAd getAdNews ", log.Any("info", adnews), log.Any("uid", uid))
if len(adnews) == 0 {
log.Warn("GetAd getAdNews result is null", log.Any("uid", uid))
return adVids
}
adVids = vidhelpser.GetVideosByIDs(uid, common.String2ObjectID(adnews))
log.Info("GetAd GetVideosByIDs ", log.Any("adVids", adVids), log.Any("uid", uid))
common.Go(func() {
b, err := json.Marshal(adVids)
if err != nil {
return
}
_ = appg.Redis.Set(key, b, redisconst.VidAdKeyExpire)
})
return adVids
}
func encodeUserList(uids []uint64, mInfo map[uint64]*vidmod.Publisher, mColle map[uint64]int) []recommod.BaseInfo {
datas := make([]recommod.BaseInfo, len(uids))
for i, v := range uids {
var name, gender, portrait string
var hasLocked, hasBanned bool
if mInfo[v] != nil {
name = mInfo[v].Name
gender = mInfo[v].Gender
portrait = mInfo[v].Portrait
hasLocked = mInfo[v].HasLocked
hasBanned = mInfo[v].HasBanned
}
datas[i] = recommod.BaseInfo{
UID: v,
Name: name,
Gender: gender,
Portrait: portrait,
HasLocked: hasLocked,
HasBanned: hasBanned,
CollectionCount: mColle[v],
}
}
return datas
}
// GetUserList 获取推荐用户列表
func GetUserList(uid uint64, page, size uint64) (code stderr.Code, data interface{}) {
uInfo, _, err := userrecomod.GetUserReco(page, size, false)
if err != nil {
return stderr.ErrDbQueryError, nil
}
uids := make([]uint64, len(uInfo))
for i, v := range uInfo {
uids[i] = v.UID
}
var wg sync.WaitGroup
wg.Add(2)
mInfo := make(map[uint64]*vidmod.Publisher)
mColle := make(map[uint64]int)
common.Go(func() {
defer wg.Done()
mInfo = vidhelpser.GetUserInfo2Map(uid, uids)
})
common.Go(func() {
defer wg.Done()
mColle, _ = vidmod.GetHisVideoCount(uids)
})
wg.Wait()
list := encodeUserList(uids, mInfo, mColle)
data = recommod.UserListResp{List: list, HasNext: true}
return stderr.Success, data
}
var cachlightVideoResponse *proto.LightVideoRes
// GetLightVidList 获取轻量视频(无用户uid)
func GetLightVidList(ip string) (code stderr.Code, data interface{}) {
code = stderr.Failure
excludedModules, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true)
if err != nil {
log.Error("GetLightVidList excluded modules err", log.E(err))
return
}
if cachlightVideoResponse == nil || len(excludedModules) > 0 || rand.Intn(100) == 50 { //随机更新缓存
hot, err := vidmod.HotVideo()
if err != nil {
return
}
vids := make([]primitive.ObjectID, len(hot))
for i, v := range hot {
vids[i] = v.ID
}
tags, _, err := tagmod.GetTagListMostPlayed(commod.Page{PageNumber: 1, PageSize: 10, Sort: nil})
if err != nil {
return
}
resVid := make([]*vidmod.VideoModel, 0)
for i := range tags {
longVid, err := vidmod.VideoListByTagIDAndPlayTime(tags[i].ID, 1, vids)
if err != nil {
continue
}
for _, v := range longVid {
vids = append(vids, v.ID)
}
resVid = append(resVid, longVid...)
shortVid, err := vidmod.VideoListByTagIDAndPlayTime(tags[i].ID, 2, vids)
if err != nil {
continue
}
for _, v := range shortVid {
vids = append(vids, v.ID)
}
resVid = append(resVid, shortVid...)
}
resp, err := adser.AdvertiseThreeServer(0, ip)
if err != nil {
log.Error("AdvertiseThreeServer", log.E(err))
return
}
ads := make([]*proto.AdsInfo, len(resp.AdvertiseList))
for i, v := range resp.AdvertiseList {
ads[i] = &proto.AdsInfo{
ID: strconv.FormatInt(v.Id, 10),
Title: v.Title,
Cover: v.CoverImg,
Href: v.Link,
Position: int(v.LocId),
SortCode: int(v.Sort),
}
}
cachlightVideoResponse = &proto.LightVideoRes{
HomeVideo: vidhelpser.EncodeVideoInfoNoUIDForRecommend(hot),
ZoneVideo: vidhelpser.EncodeVideoInfoNoUIDForRecommend(resVid),
ADs: ads,
}
}
return stderr.Success, cachlightVideoResponse
}
func GetShortDiscoverList(uid uint64, req *recmdtag.AppGetShortDiscoverListReq) (resp recmdtag.AppGetShortDiscoverListRep, code stderr.Code, err error) {
var tmpData struct {
VidList []*vidmod.VideoModel `json:"vidList"`
TagList []*recmdtag.TagInfo `json:"tagList"`
HasNext bool `json:"hasNext"`
Loaded bool `json:"-"`
}
recommendScene := req.Type != 2
excludedModules, excludeErr := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), recommendScene)
if excludeErr != nil {
return resp, stderr.ErrDbQueryError, excludeErr
}
useCache := len(excludedModules) == 0
key := redisconst.GetShortVideoListCacheKey(req.Type, req.TagID, req.PageSize, req.PageNumber)
if useCache {
str, cacheErr := appg.Redis.Get(key)
if cacheErr != nil {
log.Error("service GetShortVideoList get cache error:", log.E(cacheErr))
}
if str != nil {
if cacheErr = json.Unmarshal([]byte(*str), &tmpData); cacheErr != nil {
log.Error("service GetShortVideoList cache unmarshal error:", log.E(cacheErr))
} else {
tmpData.Loaded = true
}
}
}
if !tmpData.Loaded {
// 从数据库查询
var (
filter = bson.M{"status": vidmod.CheckPass, "newsType": vidmod.SHORT}
sort bson.D
)
// 1-最热 2-最新 3-推荐 4 热卖 5 指定tag
switch req.Type {
case 2: // 最新
if req.PageNumber > 100 {
return resp, stderr.Success, nil
}
filter["reviewAt"] = bson.M{"$gte": time.Now().AddDate(-1, 0, 0)}
sort = bson.D{{Key: "reviewAt", Value: -1}}
case 3: // 推荐
filter["reviewAt"] = bson.M{"$gte": time.Now().AddDate(0, 0, -30)}
sort = bson.D{{"chosen", -1}, {Key: "likeCount", Value: -1}, {"reviewAt", -1}}
case 4: // 热卖
filter["coins"] = bson.M{"$gt": 0}
filter["freeArea"] = false
sort = bson.D{{Key: "purchaseCount", Value: -1}}
case 5: // 指定标签
tid, err := primitive.ObjectIDFromHex(req.TagID)
if err != nil {
return resp, stderr.ErrDbQueryError, err
}
filter["tags"] = tid
sort = bson.D{{Key: "likeCount", Value: -1}, {"reviewAt", -1}}
case 1: // 热门推荐
fallthrough
default:
if req.PageNumber == 1 { //获取推荐标签列表
var s, k int64 = 0, 12
//var t = recmdtag.KindShortVideo
var t = recmdtag.Def
match := recmdtag.TypesMatch{Types: &t}
tags, err := recmdtag.List(recmdtag.Sort_sortCode_N1, &s, &k, match.New())
if err != nil {
return resp, stderr.ErrDbQueryError, err
}
for _, tag := range tags {
tmpData.TagList = append(tmpData.TagList, &recmdtag.TagInfo{
ID: tag.TID,
Name: tag.Name,
})
}
}
filter["reviewAt"] = bson.M{"$gt": time.Now().AddDate(0, -3, 0)}
sort = bson.D{{Key: "likeCount", Value: -1}, {"reviewAt", -1}}
}
if len(excludedModules) > 0 {
filter["mId"] = bson.M{"$nin": excludedModules}
}
opt := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit())).SetSort(sort)
tmpData.VidList, tmpData.HasNext, err = vidmod.FindList(filter, opt)
if err != nil {
return resp, stderr.ErrDbQueryError, err
}
if useCache {
b, marshalErr := json.Marshal(tmpData)
if marshalErr != nil {
log.Error("service GetShortVideoList cache json marshal error:", log.E(marshalErr))
return resp, stderr.Failure, marshalErr
}
_ = appg.Redis.Set(key, string(b), redisconst.ShortVideoListCacheExpire)
}
}
if len(tmpData.VidList) > 0 {
if recommendScene {
resp.Videos = vidhelpser.EncodeVideoInfoForRecommend(uid, tmpData.VidList)
} else {
resp.Videos = vidhelpser.EncodeVideoInfo(uid, tmpData.VidList)
}
}
resp.Tags = tmpData.TagList
resp.HasNext = tmpData.HasNext
return resp, stderr.Success, nil
}