424 lines
12 KiB
Go
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
|
|
}
|