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 }