1687 lines
66 KiB
Go
1687 lines
66 KiB
Go
package videoser
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"videoweb/app/api/proto"
|
|
"videoweb/app/service/configser"
|
|
"videoweb/app/service/moreser"
|
|
"videoweb/app/service/reportser"
|
|
"videoweb/app/service/slugser"
|
|
"videoweb/app/service/translationser"
|
|
"videoweb/pub/common/enum/redisconst"
|
|
"videoweb/pub/common/log"
|
|
"videoweb/pub/common/netutil"
|
|
"videoweb/pub/common/response"
|
|
"videoweb/pub/common/srvdef"
|
|
"videoweb/pub/common/useragent"
|
|
"videoweb/pub/common/utils"
|
|
"videoweb/pub/models"
|
|
"videoweb/pub/models/contenttagmod"
|
|
"videoweb/pub/models/videocategorymod"
|
|
"videoweb/pub/models/videofavoritemod"
|
|
"videoweb/pub/models/videoinformationmod"
|
|
"videoweb/pub/models/videolikemod"
|
|
"videoweb/pub/pubdef"
|
|
"videoweb/pub/redis"
|
|
"videoweb/pub/storage/proxy"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
videoGuessMinPageSize int64 = 8
|
|
videoGuessMaxPageSize int64 = 100
|
|
homeVideoDataLimit int64 = 12
|
|
queryVideoDefaultPageSize int64 = 20
|
|
queryVideoMaxPageSize int64 = 100
|
|
queryVideoAdInterval int64 = 9
|
|
videoRecommendAdInterval int64 = 8
|
|
videoListAdInterval int64 = 5
|
|
)
|
|
|
|
// VideoCategoryList 查询全部已启用视频分类
|
|
func VideoCategoryList(ctx context.Context) (*proto.VideoCategoryListResp, *srvdef.SrvErr) {
|
|
list, err := videocategorymod.FindManyVideoCategorys(ctx,
|
|
bson.M{videocategorymod.FN.VideoCategory.Status: true},
|
|
options.Find().
|
|
SetProjection(bson.D{
|
|
{Key: videocategorymod.FN.VideoCategory.Id, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.Name, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.Slug, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.Title, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.Desc, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.Rank, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.SeoInfo.Title, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.SeoInfo.Description, Value: 1},
|
|
{Key: videocategorymod.FN.VideoCategory.SeoInfo.Keyworld, Value: 1},
|
|
}).
|
|
SetSort(bson.D{
|
|
{Key: videocategorymod.FN.VideoCategory.Rank, Value: -1},
|
|
{Key: videocategorymod.FN.VideoCategory.Id, Value: -1},
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &proto.VideoCategoryListResp{List: make([]proto.VideoCategoryListItem, 0, len(list))}
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
resp.List = append(resp.List, proto.VideoCategoryListItem{
|
|
Id: item.Id, Name: item.Name, Slug: item.Slug,
|
|
Title: item.Title, Desc: item.Desc, Rank: item.Rank, SeoInfo: item.SeoInfo,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// VideoGuessList 分页查询猜你喜欢视频,按真实与后台配置观看量之和排序
|
|
func VideoGuessList(ctx context.Context, req *proto.VideoGuessListReq) (*proto.VideoGuessListResp, *srvdef.SrvErr) {
|
|
if req == nil || req.Id < 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "视频ID无效")
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, videoGuessMinPageSize, videoGuessMaxPageSize)
|
|
if pageSize < videoGuessMinPageSize {
|
|
pageSize = videoGuessMinPageSize
|
|
}
|
|
filter := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}
|
|
if req.Id > 0 {
|
|
filter[videoinformationmod.FN.VideoInformation.Id] = bson.M{"$ne": req.Id}
|
|
}
|
|
total, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &proto.VideoGuessListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: total,
|
|
PageNum: int(pageNum),
|
|
PageSize: int(pageSize),
|
|
List: make([]*proto.VideoRecommendListItem, 0),
|
|
}}
|
|
if total == 0 || pageNum > (total-1)/pageSize+1 {
|
|
return resp, nil
|
|
}
|
|
list := make(videoinformationmod.VideoInformationList, 0)
|
|
if err := videoinformationmod.AggregateVideoInformations(ctx, &list, videoGuessPipeline(filter, (pageNum-1)*pageSize, pageSize)); err != nil {
|
|
return nil, err
|
|
}
|
|
cdn := configser.ImageCdnFromCtx(ctx)
|
|
items := make([]*proto.VideoRecommendListItem, 0, len(list))
|
|
for _, item := range list {
|
|
if item != nil {
|
|
items = append(items, videoToRecommendListItem(item, cdn))
|
|
}
|
|
}
|
|
resp.List = items
|
|
return resp, nil
|
|
}
|
|
|
|
// videoGuessPipeline 按视频展示观看量排序,并使用ID稳定同分顺序
|
|
func videoGuessPipeline(filter bson.M, skip, limit int64) []bson.M {
|
|
return []bson.M{
|
|
{"$match": filter},
|
|
{"$addFields": bson.M{"watchScore": bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.WatchTimes, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 0}},
|
|
}}}},
|
|
{"$sort": bson.D{{Key: "watchScore", Value: -1}, {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1}}},
|
|
{"$skip": skip},
|
|
{"$limit": limit},
|
|
{"$project": queryVideoProjection()},
|
|
}
|
|
}
|
|
|
|
// videoRankAggItem 视频排行榜聚合结果,只保留榜单展示需要的轻量字段
|
|
type videoRankAggItem struct {
|
|
Id int64 `bson:"_id"`
|
|
Title string `bson:"title"`
|
|
Slug string `bson:"slug"`
|
|
AddedTime time.Time `bson:"addedTime"`
|
|
UpdatedAt time.Time `bson:"updatedAt"`
|
|
CoverImg string `bson:"coverImg"`
|
|
Tags []int64 `bson:"tags"`
|
|
Real pubdef.Flow `bson:"real"`
|
|
Fake pubdef.FakeFlow `bson:"fake"`
|
|
RankScore int64 `bson:"rankScore"`
|
|
}
|
|
|
|
// VideoList 分页查询已上架视频,每五条视频后穿插一条广告
|
|
func VideoList(ctx context.Context, req *proto.VideoListReq) (*proto.VideoListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
|
}
|
|
if req.CategoryId < 0 || (req.SortType != 0 && req.SortType != 1 && req.SortType != 2) {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "分类或排序方式无效", "")
|
|
}
|
|
filter := videoListFilter(req.CategoryId)
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
|
|
videoCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
advitise := configser.GetAdvitise()
|
|
ads := advitise.PostList
|
|
adCount := videoListInsertedAdsCount(videoCount, len(ads))
|
|
mixedCount := videoCount + adCount
|
|
resp := &proto.VideoListResp{
|
|
BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
|
|
List: make([]*proto.VideoRecommendListItem, 0),
|
|
},
|
|
Advitise: advitise,
|
|
}
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedCount {
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedCount {
|
|
end = mixedCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, videoCount, adCount, videoListAdInterval)
|
|
list := make(videoinformationmod.VideoInformationList, 0)
|
|
if contentLimit > 0 {
|
|
list, err = findVideoRecommendPage(ctx, filter, videoListQuerySort(req.SortType), contentStart, contentLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
resp.List = buildVideoListMixedPage(list, ads, configser.GetRequestConfig(ctx).ImgCdn,
|
|
start, end, contentStart, adCount)
|
|
return resp, nil
|
|
}
|
|
|
|
// videoListFilter 按上架状态和分类构建筛选条件
|
|
func videoListFilter(categoryId int64) bson.M {
|
|
filter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}
|
|
if categoryId > 0 {
|
|
filter[videoinformationmod.FN.VideoInformation.CategoryId] = categoryId
|
|
}
|
|
return filter
|
|
}
|
|
|
|
// videoListSort 根据热度或最近更新时间生成稳定的视频顺序
|
|
func videoListSort(sortType int64) bson.D {
|
|
if sortType == 2 {
|
|
return bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
}
|
|
return bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
}
|
|
|
|
// videoListQuerySort 将后台热度置于原观看数顺序之前
|
|
func videoListQuerySort(sortType int64) bson.D {
|
|
sort := videoListSort(sortType)
|
|
if sortType == 2 {
|
|
return sort
|
|
}
|
|
return append(bson.D{{Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1}}, sort...)
|
|
}
|
|
|
|
// videoListInsertedAdsCount 只在仍有后续视频时插入广告,素材不足时循环使用
|
|
func videoListInsertedAdsCount(videoCount int64, adsCount int) int64 {
|
|
if videoCount <= 1 || adsCount == 0 {
|
|
return 0
|
|
}
|
|
return (videoCount - 1) / videoListAdInterval
|
|
}
|
|
|
|
// buildVideoListMixedPage 根据全局位置组装视频与广告,保持跨页间隔一致
|
|
func buildVideoListMixedPage(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string,
|
|
start, end, contentStart, adCount int64) []*proto.VideoRecommendListItem {
|
|
items := make([]*proto.VideoRecommendListItem, 0, end-start)
|
|
contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
|
|
for i, item := range list {
|
|
contentByIndex[contentStart+int64(i)] = item
|
|
}
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, videoListAdInterval)
|
|
if adIndex >= 0 && len(ads) > 0 {
|
|
items = append(items, videoAdToRecommendListItem(ads[adIndex%int64(len(ads))], cdn))
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, videoListAdInterval)
|
|
if item := contentByIndex[contentIndex]; item != nil {
|
|
items = append(items, videoToRecommendListItem(item, cdn))
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
type videoRankCount struct {
|
|
Total int64 `bson:"total"`
|
|
}
|
|
|
|
// incrementVideoWatchCount 更新视频总观看、展示和日/周/月/年窗口观看数。
|
|
// 周/月/年窗口按北京时间切:周一 00:00、每月 1 日 00:00、每年 1 月 1 日 00:00。
|
|
func incrementVideoWatchCount(videoInfo *videoinformationmod.VideoInformation) {
|
|
if videoInfo == nil {
|
|
return
|
|
}
|
|
now := utils.Now()
|
|
incDoc := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.ShowTimes: 1,
|
|
}
|
|
setDoc := bson.M{
|
|
videoinformationmod.FN.VideoInformation.RealUpdateAt: now,
|
|
}
|
|
if videoInfo.RealUpdateAt.After(utils.BeginOfToday()) {
|
|
incDoc[videoinformationmod.FN.VideoInformation.Real.TodayWatchs] = 1
|
|
} else {
|
|
setDoc[videoinformationmod.FN.VideoInformation.Real.TodayWatchs] = 1
|
|
}
|
|
if videoInfo.RealUpdateAt.After(utils.BeginOfWeekByTime(now)) {
|
|
incDoc[videoinformationmod.FN.VideoInformation.Real.WeekWatchs] = 1
|
|
} else {
|
|
setDoc[videoinformationmod.FN.VideoInformation.Real.WeekWatchs] = 1
|
|
}
|
|
if videoInfo.RealUpdateAt.After(utils.BeginOfMonth(now)) {
|
|
incDoc[videoinformationmod.FN.VideoInformation.Real.MonthWatchs] = 1
|
|
} else {
|
|
setDoc[videoinformationmod.FN.VideoInformation.Real.MonthWatchs] = 1
|
|
}
|
|
if videoInfo.RealUpdateAt.After(utils.BeginOfYear(now)) {
|
|
incDoc[videoinformationmod.FN.VideoInformation.Real.YearWatchs] = 1
|
|
} else {
|
|
setDoc[videoinformationmod.FN.VideoInformation.Real.YearWatchs] = 1
|
|
}
|
|
if err := videoinformationmod.UpdateOneVideoInformation(context.Background(), bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: videoInfo.Id,
|
|
}, bson.M{"$set": setDoc, "$inc": incDoc}); err != nil {
|
|
log.Warn("incrementVideoWatchCount", log.Any("videoId", videoInfo.Id), log.E(err))
|
|
}
|
|
}
|
|
|
|
// HomeVideoDataForHome 查询总首页推荐、最新、最热三组视频数据
|
|
func HomeVideoDataForHome(ctx context.Context, imgCdn string) (proto.HomeVideoDataResp, *srvdef.SrvErr) {
|
|
resp := proto.HomeVideoDataResp{
|
|
RecommendList: make([]proto.VideoInfo, 0),
|
|
NewestList: make([]proto.VideoInfo, 0),
|
|
HotList: make([]proto.VideoInfo, 0),
|
|
}
|
|
var (
|
|
recommendList videoinformationmod.VideoInformationList
|
|
newestList videoinformationmod.VideoInformationList
|
|
hotList videoinformationmod.VideoInformationList
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
err1 *srvdef.SrvErr
|
|
)
|
|
setErr := func(err *srvdef.SrvErr) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if err1 == nil {
|
|
err1 = err
|
|
}
|
|
}
|
|
queryList := func(sortType videoinformationmod.VideoSortType, target *videoinformationmod.VideoInformationList) {
|
|
defer wg.Done()
|
|
list, err := homeVideoDataList(ctx, sortType)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
*target = list
|
|
}
|
|
wg.Add(3)
|
|
go queryList(videoinformationmod.VideoSortType_Recommend, &recommendList)
|
|
go queryList(videoinformationmod.VideoSortType_New, &newestList)
|
|
go queryList(videoinformationmod.VideoSortType_Hot, &hotList)
|
|
wg.Wait()
|
|
if err1 != nil {
|
|
return resp, err1
|
|
}
|
|
resp.RecommendList = videoListToHomeData(recommendList, imgCdn)
|
|
resp.NewestList = videoListToHomeData(newestList, imgCdn)
|
|
resp.HotList = videoListToHomeData(hotList, imgCdn)
|
|
return resp, nil
|
|
}
|
|
|
|
// QueryVideoMore 根据分类id分页查询视频列表,并按固定间隔穿插列表广告
|
|
func QueryVideoMore(ctx context.Context, req *proto.QueryVideoReq) (*proto.QueryVideoResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
categoryId, err := slugser.VideoCategoryID(ctx, req.CategoryId, req.CategorySlug, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.CategoryId = categoryId
|
|
categoryInfo := videoCategoryInfo(req.CategoryId)
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
|
|
filter, sort, err := queryVideoFilterAndSort(ctx, req.CategoryId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
ads := configser.GetAdvitise().PostList
|
|
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ads) == 0 {
|
|
resp, err := queryVideoMoreWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
|
|
if resp != nil {
|
|
resp.CategoryInfo = categoryInfo
|
|
}
|
|
return resp, err
|
|
}
|
|
adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), queryVideoAdInterval)
|
|
mixedTotalCount := totalCount + adsTotalCount
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedTotalCount || totalCount == 0 {
|
|
resp := emptyQueryVideoMorePage(pageNum, pageSize, categoryInfo)
|
|
resp.TotalCount = mixedTotalCount
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedTotalCount {
|
|
end = mixedTotalCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, queryVideoAdInterval)
|
|
list := videoinformationmod.VideoInformationList{}
|
|
if contentLimit > 0 {
|
|
list, err = findVideoCategoryPage(ctx, filter, sort, req.CategoryId, contentStart, contentLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
resp := buildQueryVideoMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
|
|
return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedTotalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: resp,
|
|
}, CategoryInfo: categoryInfo, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
|
|
}
|
|
|
|
// VideoRecommendList 分页查询 app 端推荐视频列表,并按固定间隔穿插列表广告
|
|
func VideoRecommendList(ctx context.Context, req *proto.VideoRecommendListReq) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
categoryId, err := slugser.VideoCategoryID(ctx, req.CategoryId, req.CategorySlug, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagId, err := slugser.ContentTagID(ctx, req.TagId, req.TagSlug, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.CategoryId = categoryId
|
|
req.TagId = tagId
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
|
|
filter, sort, err := videoRecommendFilterAndSort(ctx, req.CategoryId, req.TagId, req.SortList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
ads := configser.GetAdvitise().PostList
|
|
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ads) == 0 {
|
|
return videoRecommendListWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
|
|
}
|
|
adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), videoRecommendAdInterval)
|
|
mixedTotalCount := totalCount + adsTotalCount
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedTotalCount || totalCount == 0 {
|
|
resp := emptyVideoRecommendListPage(pageNum, pageSize)
|
|
resp.TotalCount = mixedTotalCount
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedTotalCount {
|
|
end = mixedTotalCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, videoRecommendAdInterval)
|
|
list := videoinformationmod.VideoInformationList{}
|
|
if contentLimit > 0 {
|
|
list, err = findVideoCategoryPage(ctx, filter, sort, req.CategoryId, contentStart, contentLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
items := buildVideoRecommendMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
|
|
return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedTotalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}}, nil
|
|
}
|
|
|
|
// VideoTagList 根据统一标签ID分页查询视频列表,并按固定间隔穿插列表广告
|
|
func VideoTagList(ctx context.Context, req *proto.ContentTagPageReq) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
|
}
|
|
tagId, err := slugser.ContentTagID(ctx, req.Id, req.Slug, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Id = tagId
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
|
|
ok, err := configser.HasEnabledContentTagBind(ctx, req.Id, contenttagmod.ContentTagTypeVideo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return emptyVideoRecommendListPage(pageNum, pageSize), nil
|
|
}
|
|
filter := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
videoinformationmod.FN.VideoInformation.Tags: req.Id,
|
|
}
|
|
sort := bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
ads := configser.GetAdvitise().PostList
|
|
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ads) == 0 {
|
|
return videoRecommendListWithoutAds(ctx, filter, sort, 0, pageNum, pageSize, totalCount, config.ImgCdn)
|
|
}
|
|
adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), videoRecommendAdInterval)
|
|
mixedTotalCount := totalCount + adsTotalCount
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedTotalCount || totalCount == 0 {
|
|
resp := emptyVideoRecommendListPage(pageNum, pageSize)
|
|
resp.TotalCount = mixedTotalCount
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedTotalCount {
|
|
end = mixedTotalCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, videoRecommendAdInterval)
|
|
list := videoinformationmod.VideoInformationList{}
|
|
if contentLimit > 0 {
|
|
list, err = findVideoRecommendPage(ctx, filter, sort, contentStart, contentLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
items := buildVideoRecommendMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
|
|
return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedTotalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}}, nil
|
|
}
|
|
|
|
// FavoriteVideo 当前登录用户收藏或取消收藏指定视频
|
|
func FavoriteVideo(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.FavoriteVideoReq) (*proto.FavoriteVideoResp, *srvdef.SrvErr) {
|
|
if req == nil || userId <= 0 || req.Id <= 0 || req.IsFavorite == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: req.Id,
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}, options.FindOne().SetProjection(bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.CategoryId: 1,
|
|
videoinformationmod.FN.VideoInformation.MediaType: 1,
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if *req.IsFavorite {
|
|
created, err := videofavoritemod.UpsertOneVideoFavorite(ctx, userId, req.Id, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if created {
|
|
clearFavoriteVideoListCache(userId)
|
|
if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: 1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := reportser.ReportVideoCollect(userId, ua, ip, videoInfo, 1); err != nil {
|
|
log.Warn("ReportVideoCollect failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
} else {
|
|
deleted, err := videofavoritemod.DeleteOneVideoFavorite(ctx, bson.M{
|
|
videofavoritemod.FN.VideoFavorite.UserID: userId,
|
|
videofavoritemod.FN.VideoFavorite.VideoID: req.Id,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deleted {
|
|
clearFavoriteVideoListCache(userId)
|
|
if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: -1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := reportser.ReportVideoCollect(userId, ua, ip, videoInfo, 2); err != nil {
|
|
log.Warn("ReportVideoCollect failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
}
|
|
return &proto.FavoriteVideoResp{Id: req.Id, IsFavorite: *req.IsFavorite}, nil
|
|
}
|
|
|
|
// LikeVideo 当前登录用户点赞或取消点赞指定视频
|
|
func LikeVideo(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.LikeVideoReq) (*proto.LikeVideoResp, *srvdef.SrvErr) {
|
|
if req == nil || userId <= 0 || req.Id <= 0 || req.IsLike == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: req.Id,
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}, options.FindOne().SetProjection(bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.CategoryId: 1,
|
|
videoinformationmod.FN.VideoInformation.MediaType: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.TaskId: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.FileId: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.Url: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.H265Url: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.VideoTime: 1,
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if *req.IsLike {
|
|
created, err := videolikemod.UpsertOneVideoLike(ctx, userId, req.Id, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if created {
|
|
videoInfo.Real.Likes++
|
|
if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: 1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := reportser.ReportVideoLike(userId, ua, ip, videoInfo, 1); err != nil {
|
|
log.Warn("ReportVideoLike failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
} else {
|
|
deleted, err := videolikemod.DeleteOneVideoLike(ctx, bson.M{
|
|
videolikemod.FN.VideoLike.UserID: userId,
|
|
videolikemod.FN.VideoLike.VideoID: req.Id,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deleted {
|
|
if videoInfo.Real.Likes > 0 {
|
|
videoInfo.Real.Likes--
|
|
if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: -1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := reportser.ReportVideoLike(userId, ua, ip, videoInfo, 2); err != nil {
|
|
log.Warn("ReportVideoLike failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
}
|
|
return &proto.LikeVideoResp{
|
|
Id: req.Id,
|
|
IsLike: *req.IsLike,
|
|
LikeNum: videoInfo.Real.Likes + videoInfo.Fake.Likes,
|
|
}, nil
|
|
}
|
|
|
|
// QueryFavoriteVideoList 分页查询当前登录用户收藏的视频,按收藏时间倒序返回
|
|
func QueryFavoriteVideoList(ctx context.Context, userId int64, req *proto.QueryFavoriteVideoListReq) (*proto.QueryFavoriteVideoListResp, *srvdef.SrvErr) {
|
|
if userId <= 0 || req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
pageNum, pageSize := normalizeQueryVideoPage(req.PageNum, req.PageSize)
|
|
version := getFavoriteVideoListCacheVersion(userId)
|
|
cacheKey := redisconst.FavoriteVideoListCacheKey(userId, version, pageNum, pageSize)
|
|
if resp := getFavoriteVideoListCache(cacheKey); resp != nil {
|
|
return resp, nil
|
|
}
|
|
filter := bson.M{videofavoritemod.FN.VideoFavorite.UserID: userId}
|
|
totalCount, err := videofavoritemod.CountVideoFavorite(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
favorites, err := videofavoritemod.FindManyVideoFavorite(ctx, filter,
|
|
options.Find().
|
|
SetSort(bson.D{{Key: videofavoritemod.FN.VideoFavorite.CreatedAt, Value: -1}, {Key: videofavoritemod.FN.VideoFavorite.Id, Value: -1}}).
|
|
SetSkip((pageNum-1)*pageSize).
|
|
SetLimit(pageSize).
|
|
SetProjection(bson.M{videofavoritemod.FN.VideoFavorite.VideoID: 1}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(favorites) == 0 {
|
|
resp := emptyFavoriteVideoPage(pageNum, pageSize)
|
|
resp.TotalCount = totalCount
|
|
setFavoriteVideoListCache(cacheKey, resp)
|
|
return resp, nil
|
|
}
|
|
ids := make([]int64, 0, len(favorites))
|
|
for _, favorite := range favorites {
|
|
ids = append(ids, favorite.VideoID)
|
|
}
|
|
videoList, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}, options.Find().SetProjection(favoriteVideoProjection()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
videoMap := make(map[int64]*videoinformationmod.VideoInformation, len(videoList))
|
|
for _, item := range videoList {
|
|
videoMap[item.Id] = item
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
list := make([]*proto.FavoriteVideoListItem, 0, len(favorites))
|
|
for _, favorite := range favorites {
|
|
item, ok := videoMap[favorite.VideoID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
list = append(list, favoriteVideoListItem(item, config.ImgCdn))
|
|
}
|
|
resp := &proto.QueryFavoriteVideoListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: totalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: list,
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
|
|
setFavoriteVideoListCache(cacheKey, resp)
|
|
return resp, nil
|
|
}
|
|
|
|
// QueryVideoRank 分页查询视频排行榜,读取独立的 10 分钟内存缓存。
|
|
// 点赞榜按累计点赞量、周/月/年榜按对应周期观看量取前 50;不足时随机补位。分页 pageSize 不是 50 时强制为 50。
|
|
func QueryVideoRank(ctx context.Context, req *proto.VideoRankReq) (*proto.VideoRankResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
period := proto.VideoRankPeriod(req.RankPeriod)
|
|
if period < proto.VideoRankPeriodLike || period > proto.VideoRankPeriodYear {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
pageNum, pageSize := configser.NormalizeRankPage(req.PageNum, req.PageSize)
|
|
all := configser.GetVideoRankList(configser.RankPeriod(period), 0)
|
|
return &proto.VideoRankResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: int64(len(all)),
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: configser.SliceRankPage(all, pageNum, pageSize),
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
|
|
}
|
|
|
|
// countVideoTotalLikeRank 统计累计点赞榜可展示视频数量
|
|
func countVideoTotalLikeRank(ctx context.Context) (int64, *srvdef.SrvErr) {
|
|
rows := make([]videoRankCount, 0)
|
|
if err := videoinformationmod.AggregateVideoInformations(ctx, &rows, videoTotalLikeRankCountPipeline()); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return 0, nil
|
|
}
|
|
return rows[0].Total, nil
|
|
}
|
|
|
|
// aggregateVideoTotalLikeRank 分页查询累计点赞榜视频
|
|
func aggregateVideoTotalLikeRank(ctx context.Context, skip, limit int64) ([]videoRankAggItem, *srvdef.SrvErr) {
|
|
list := make([]videoRankAggItem, 0)
|
|
if err := videoinformationmod.AggregateVideoInformations(ctx, &list, videoTotalLikeRankPagePipeline(skip, limit)); err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// countVideoPeriodLikeRank 统计周期新增点赞榜可展示视频数量
|
|
func countVideoPeriodLikeRank(ctx context.Context, since time.Time) (int64, *srvdef.SrvErr) {
|
|
rows := make([]videoRankCount, 0)
|
|
if err := videolikemod.AggregateVideoLike(ctx, &rows, videoPeriodLikeRankCountPipeline(since)); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return 0, nil
|
|
}
|
|
return rows[0].Total, nil
|
|
}
|
|
|
|
// aggregateVideoPeriodLikeRank 分页查询周期新增点赞榜视频
|
|
func aggregateVideoPeriodLikeRank(ctx context.Context, since time.Time, skip, limit int64) ([]videoRankAggItem, *srvdef.SrvErr) {
|
|
list := make([]videoRankAggItem, 0)
|
|
if err := videolikemod.AggregateVideoLike(ctx, &list, videoPeriodLikeRankPagePipeline(since, skip, limit)); err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// getFavoriteVideoListCache 读取当前用户收藏视频分页缓存,缓存异常时降级查库
|
|
func getFavoriteVideoListCache(cacheKey string) *proto.QueryFavoriteVideoListResp {
|
|
cacheData, err := redis.Get(cacheKey)
|
|
if err != nil {
|
|
log.Warn("读取视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
return nil
|
|
}
|
|
if cacheData == nil || *cacheData == "" {
|
|
return nil
|
|
}
|
|
resp := &proto.QueryFavoriteVideoListResp{}
|
|
if err := json.Unmarshal([]byte(*cacheData), resp); err != nil {
|
|
log.Warn("解析视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
return nil
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// setFavoriteVideoListCache 写入当前用户收藏视频分页缓存,缓存异常不影响主流程
|
|
func setFavoriteVideoListCache(cacheKey string, resp *proto.QueryFavoriteVideoListResp) {
|
|
if resp == nil {
|
|
return
|
|
}
|
|
bytes, err := json.Marshal(resp)
|
|
if err != nil {
|
|
log.Warn("序列化视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
return
|
|
}
|
|
if err := redis.Set(cacheKey, string(bytes), redisconst.FavoriteVideoListKeyExpire); err != nil {
|
|
log.Warn("写入视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
}
|
|
}
|
|
|
|
// getFavoriteVideoListCacheVersion 读取当前用户视频收藏列表缓存版本,读取失败时使用默认版本降级查库
|
|
func getFavoriteVideoListCacheVersion(userId int64) int64 {
|
|
cacheKey := redisconst.FavoriteVideoListVersionCacheKey(userId)
|
|
cacheData, err := redis.Get(cacheKey)
|
|
if err != nil {
|
|
log.Warn("读取视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
return 0
|
|
}
|
|
if cacheData == nil || *cacheData == "" {
|
|
return 0
|
|
}
|
|
version, err := strconv.ParseInt(*cacheData, 10, 64)
|
|
if err != nil {
|
|
log.Warn("解析视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.Any("cacheData", *cacheData), log.E(err))
|
|
return 0
|
|
}
|
|
return version
|
|
}
|
|
|
|
// clearFavoriteVideoListCache 刷新当前用户视频收藏列表缓存版本,旧分页缓存等待TTL自然过期
|
|
func clearFavoriteVideoListCache(userId int64) {
|
|
if userId <= 0 {
|
|
return
|
|
}
|
|
cacheKey := redisconst.FavoriteVideoListVersionCacheKey(userId)
|
|
if _, err := redis.Incr(cacheKey, redisconst.FavoriteListVersionKeyExpire); err != nil {
|
|
log.Warn("刷新视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.E(err))
|
|
}
|
|
}
|
|
|
|
// QueryVideoInfo 查询视频详情、收藏状态和点赞状态
|
|
func QueryVideoInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryVideoInfoReq) (*proto.QueryVideoInfoResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
videoId, err := slugser.VideoID(ctx, req.Id, req.Slug, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Id = videoId
|
|
config := configser.GetRequestConfig(ctx)
|
|
videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: req.Id,
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}, options.FindOne().SetProjection(videoDetailProjection()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 播放详情访问量异步累加,避免写操作阻塞详情读取链路
|
|
go incrementVideoWatchCount(videoInfo)
|
|
// 并发查询收藏状态、点赞状态和收藏总数
|
|
isFavorite, isLike, countCollect, err := queryVideoDetailExtraData(ctx, userId, videoInfo.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.QueryVideoInfoResp{
|
|
Path: videoRequestPath(ctx),
|
|
Translations: translationser.GetTranslationData(),
|
|
Config: config,
|
|
BottonTranslations: translationser.GetTranslationBottomList(),
|
|
Advitise: configser.GetAdvitise(),
|
|
Announcement: configser.GetAnnouncement(),
|
|
VideoInfo: videoDetailInfoData(videoInfo, config.ImgCdn, ip, ua, isFavorite, isLike, countCollect),
|
|
IsFavorite: isFavorite,
|
|
IsLike: isLike,
|
|
}, nil
|
|
}
|
|
|
|
// queryVideoDetailExtraData 并发查询详情页附加数据,缩短详情接口整体等待时间
|
|
func queryVideoDetailExtraData(ctx context.Context, userId, videoId int64) (bool, bool, int64, *srvdef.SrvErr) {
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var firstErr *srvdef.SrvErr
|
|
isFavorite := false
|
|
isLike := false
|
|
countCollect := int64(0)
|
|
setErr := func(err *srvdef.SrvErr) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
wg.Add(3)
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryVideoIsFavorite(ctx, userId, videoId)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
isFavorite = data
|
|
mu.Unlock()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryVideoIsLike(ctx, userId, videoId)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
isLike = data
|
|
mu.Unlock()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
count, err := videofavoritemod.CountVideoFavorite(ctx, bson.M{
|
|
videofavoritemod.FN.VideoFavorite.VideoID: videoId,
|
|
})
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
countCollect = count
|
|
mu.Unlock()
|
|
}()
|
|
wg.Wait()
|
|
if firstErr != nil {
|
|
return false, false, 0, firstErr
|
|
}
|
|
return isFavorite, isLike, countCollect, nil
|
|
}
|
|
|
|
// queryVideoIsFavorite 判断当前登录用户是否已收藏该视频,未登录直接返回false
|
|
func queryVideoIsFavorite(ctx context.Context, userId, videoId int64) (bool, *srvdef.SrvErr) {
|
|
if userId <= 0 {
|
|
return false, nil
|
|
}
|
|
count, err := videofavoritemod.CountVideoFavorite(ctx, bson.M{
|
|
videofavoritemod.FN.VideoFavorite.UserID: userId,
|
|
videofavoritemod.FN.VideoFavorite.VideoID: videoId,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// queryVideoIsLike 判断当前登录用户是否已点赞该视频,未登录直接返回false
|
|
func queryVideoIsLike(ctx context.Context, userId, videoId int64) (bool, *srvdef.SrvErr) {
|
|
if userId <= 0 {
|
|
return false, nil
|
|
}
|
|
count, err := videolikemod.CountVideoLike(ctx, bson.M{
|
|
videolikemod.FN.VideoLike.UserID: userId,
|
|
videolikemod.FN.VideoLike.VideoID: videoId,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// videoRankPeriodSince 将排行榜周期转换为点赞起始时间
|
|
func videoRankPeriodSince(period proto.VideoRankPeriod) time.Time {
|
|
now := time.Now()
|
|
switch period {
|
|
case proto.VideoRankPeriodWeek:
|
|
return now.AddDate(0, 0, -7)
|
|
case proto.VideoRankPeriodMonth:
|
|
return now.AddDate(0, -1, 0)
|
|
case proto.VideoRankPeriodYear:
|
|
return now.AddDate(-1, 0, 0)
|
|
default:
|
|
return time.Time{}
|
|
}
|
|
}
|
|
|
|
// videoTotalLikeRankCountPipeline 构建累计点赞榜数量统计管道
|
|
func videoTotalLikeRankCountPipeline() []bson.M {
|
|
return append(videoTotalLikeRankBasePipeline(), bson.M{"$count": "total"})
|
|
}
|
|
|
|
// videoTotalLikeRankPagePipeline 构建累计点赞榜分页查询管道
|
|
func videoTotalLikeRankPagePipeline(skip, limit int64) []bson.M {
|
|
pipeline := videoTotalLikeRankBasePipeline()
|
|
pipeline = append(pipeline,
|
|
bson.M{"$sort": bson.D{
|
|
{Key: "rankScore", Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}},
|
|
bson.M{"$skip": skip},
|
|
bson.M{"$limit": limit},
|
|
bson.M{"$project": videoRankProjection()},
|
|
)
|
|
return pipeline
|
|
}
|
|
|
|
// videoTotalLikeRankBasePipeline 构建累计点赞榜基础过滤管道
|
|
func videoTotalLikeRankBasePipeline() []bson.M {
|
|
return []bson.M{
|
|
{"$match": bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
|
|
{"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.Likes, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.Likes, 0}},
|
|
}}}},
|
|
{"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
|
|
}
|
|
}
|
|
|
|
// videoPeriodLikeRankCountPipeline 构建周期新增点赞榜数量统计管道
|
|
func videoPeriodLikeRankCountPipeline(since time.Time) []bson.M {
|
|
return append(videoPeriodLikeRankBasePipeline(since), bson.M{"$count": "total"})
|
|
}
|
|
|
|
// videoPeriodLikeRankPagePipeline 构建周期新增点赞榜分页查询管道
|
|
func videoPeriodLikeRankPagePipeline(since time.Time, skip, limit int64) []bson.M {
|
|
pipeline := videoPeriodLikeRankBasePipeline(since)
|
|
pipeline = append(pipeline,
|
|
bson.M{"$sort": bson.D{
|
|
{Key: "rankScore", Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}},
|
|
bson.M{"$skip": skip},
|
|
bson.M{"$limit": limit},
|
|
bson.M{"$project": videoRankProjection()},
|
|
)
|
|
return pipeline
|
|
}
|
|
|
|
// videoPeriodLikeRankBasePipeline 构建周期新增点赞榜基础过滤管道
|
|
func videoPeriodLikeRankBasePipeline(since time.Time) []bson.M {
|
|
return []bson.M{
|
|
{"$match": bson.M{videolikemod.FN.VideoLike.CreatedAt: bson.M{"$gte": since}}},
|
|
{"$group": bson.M{
|
|
"_id": "$" + videolikemod.FN.VideoLike.VideoID,
|
|
"rankScore": bson.M{"$sum": 1},
|
|
}},
|
|
{"$lookup": bson.M{
|
|
"from": videoinformationmod.TN.VideoInformation,
|
|
"localField": "_id",
|
|
"foreignField": videoinformationmod.FN.VideoInformation.Id,
|
|
"as": "video",
|
|
}},
|
|
{"$unwind": "$video"},
|
|
{"$match": bson.M{"video." + videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
|
|
{"$replaceRoot": bson.M{"newRoot": bson.M{"$mergeObjects": []interface{}{"$video", bson.M{"rankScore": "$rankScore"}}}}},
|
|
}
|
|
}
|
|
|
|
// videoRankProjection 限定排行榜返回字段,减少分页查询的数据传输体积
|
|
func videoRankProjection() bson.M {
|
|
return bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
|
videoinformationmod.FN.VideoInformation.UpdatedAt: 1,
|
|
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
|
videoinformationmod.FN.VideoInformation.Tags: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
"rankScore": 1,
|
|
}
|
|
}
|
|
|
|
// videoRankToContentData 将排行榜聚合结果转换为榜单展示数据
|
|
func videoRankToContentData(item videoRankAggItem, imgCdn string) proto.ContentData {
|
|
tagNames, tagSlugs := configser.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.RankScore,
|
|
}
|
|
}
|
|
|
|
// videoDetailInfoData 将视频模型转换为详情页专用响应结构,视频播放地址按m3u8授权格式返回
|
|
func videoDetailInfoData(item *videoinformationmod.VideoInformation, imgCdn, ip, ua string, isFavorite, isLike bool, countCollect int64) proto.VideoDetailInfo {
|
|
tags := make([]proto.TagsInfoData, 0, len(item.Tags))
|
|
for _, tagId := range item.Tags {
|
|
tag := configser.GetVideoTag(tagId)
|
|
if tag != nil {
|
|
tags = append(tags, *tag)
|
|
}
|
|
}
|
|
video := item.Video
|
|
if video.Url != "" {
|
|
video.Url = GetM3U8AuthParam(item.Id, video.Url, ip, ua)
|
|
}
|
|
if video.H265Url != "" {
|
|
video.H265Url = GetM3U8AuthParam(item.Id, video.H265Url, ip, ua)
|
|
}
|
|
return proto.VideoDetailInfo{
|
|
Id: item.Id,
|
|
Title: item.Title,
|
|
Slug: item.Slug,
|
|
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
|
|
Video: video,
|
|
Tags: tags,
|
|
WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
|
|
LikeNum: item.Real.Likes + item.Fake.Likes,
|
|
CountCollect: countCollect,
|
|
AddedTime: item.AddedTime,
|
|
Desc: item.Desc,
|
|
IsFavorite: isFavorite,
|
|
IsLike: isLike,
|
|
}
|
|
}
|
|
|
|
// videoDetailProjection 限定视频详情查询字段,避免返回整条视频文档
|
|
func videoDetailProjection() bson.M {
|
|
return bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.TaskId: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.FileId: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.Url: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.H265Url: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.Cover: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.Height: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.Width: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.VideoTime: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs: 1,
|
|
videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4: 1,
|
|
videoinformationmod.FN.VideoInformation.Tags: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
|
videoinformationmod.FN.VideoInformation.Desc: 1,
|
|
videoinformationmod.FN.VideoInformation.CategoryId: 1,
|
|
videoinformationmod.FN.VideoInformation.RealUpdateAt: 1,
|
|
}
|
|
}
|
|
|
|
// homeVideoDataList 按首页视频排序类型查询固定数量的视频
|
|
func homeVideoDataList(ctx context.Context, sortType videoinformationmod.VideoSortType) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
|
|
filter := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}
|
|
if sortType == videoinformationmod.VideoSortType_Recommend {
|
|
filter[videoinformationmod.FN.VideoInformation.IsRecommend] = true
|
|
}
|
|
return videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
|
|
SetProjection(queryVideoProjection()).
|
|
SetSort(homeVideoDataSort(sortType)).
|
|
SetLimit(homeVideoDataLimit))
|
|
}
|
|
|
|
// homeVideoDataSort 将首页视频推荐、最新、最热转换为 MongoDB 排序条件
|
|
func homeVideoDataSort(sortType videoinformationmod.VideoSortType) bson.D {
|
|
switch sortType {
|
|
case videoinformationmod.VideoSortType_Recommend:
|
|
return bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WeekWatchs, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
case videoinformationmod.VideoSortType_Hot:
|
|
return bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
default:
|
|
return bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}
|
|
}
|
|
}
|
|
|
|
// videoListToHomeData 批量转换视频模型为 app 首页视频数据
|
|
func videoListToHomeData(list videoinformationmod.VideoInformationList, imgCdn string) []proto.VideoInfo {
|
|
result := make([]proto.VideoInfo, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
result = append(result, homeVideoInfo(item, imgCdn))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// homeVideoSort 将分类配置的排序方式转换为MG排序条件
|
|
func homeVideoSort(sortType pubdef.CategorySortType) bson.D {
|
|
switch sortType {
|
|
case pubdef.CategorySortTypeAddedTimeDesc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, -1}}
|
|
case pubdef.CategorySortTypeWatchDesc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1}, {videoinformationmod.FN.VideoInformation.Fake.WatchTimes, -1}}
|
|
case pubdef.CategorySortTypeLikeDesc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.Real.Likes, -1}, {videoinformationmod.FN.VideoInformation.Fake.Likes, -1}}
|
|
case pubdef.CategorySortTypeAddedTimeAsc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, 1}}
|
|
case pubdef.CategorySortTypeWatchAsc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.Real.WatchTimes, 1}, {videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 1}}
|
|
case pubdef.CategorySortTypeLikeAsc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.Real.Likes, 1}, {videoinformationmod.FN.VideoInformation.Fake.Likes, 1}}
|
|
case pubdef.CategorySortTypeUpdateTimeDesc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.UpdatedAt, -1}}
|
|
case pubdef.CategorySortTypeUpdateTimeAsc:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.UpdatedAt, 1}}
|
|
case pubdef.CategorySortTypeSortType:
|
|
return videoinformationmod.CategoryRankSort()
|
|
default:
|
|
return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, -1}}
|
|
}
|
|
}
|
|
|
|
// homeVideoInfo 将视频模型转换为首页轻量视频结构
|
|
func homeVideoInfo(item *videoinformationmod.VideoInformation, imgCdn string) proto.VideoInfo {
|
|
// 标签名称来自本地配置缓存,避免为每条视频额外访问数据库
|
|
tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
|
|
// 返回
|
|
return proto.VideoInfo{ContentData: 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,
|
|
SeoInfo: pubdef.SeoValue{
|
|
Title: item.Title,
|
|
Keyworld: append([]string{}, item.Keyworld...),
|
|
Description: item.Desc,
|
|
},
|
|
}}
|
|
}
|
|
|
|
// queryVideoFilterAndSort 根据视频分类构建更多列表查询条件和排序规则
|
|
func queryVideoFilterAndSort(ctx context.Context, categoryID int64) (bson.M, bson.D, *srvdef.SrvErr) {
|
|
translationser.EnsureVideoCategoryCache(ctx)
|
|
category := translationser.GetVideoCategory(categoryID)
|
|
if category == nil || !category.Status {
|
|
return nil, nil, srvdef.New(srvdef.ErrNoData, "", "")
|
|
}
|
|
filter := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}
|
|
filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": category.Tags}
|
|
return filter, homeVideoSort(category.SortType), nil
|
|
}
|
|
|
|
// queryVideoProjection 限定视频更多列表字段,避免返回详情页才需要的大字段
|
|
func queryVideoProjection() bson.M {
|
|
return bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.Keyworld: 1,
|
|
videoinformationmod.FN.VideoInformation.Desc: 1,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
|
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
|
videoinformationmod.FN.VideoInformation.Tags: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
}
|
|
}
|
|
|
|
// videoRecommendFilterAndSort 根据分类标签、内容标签和推荐视频排序枚举构建查询条件
|
|
func videoRecommendFilterAndSort(ctx context.Context, categoryId, tagId, sortList int64) (bson.M, bson.D, *srvdef.SrvErr) {
|
|
filter := bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
}
|
|
tagConditions := make([]bson.M, 0, 2)
|
|
if categoryId > 0 {
|
|
translationser.EnsureVideoCategoryCache(ctx)
|
|
category := translationser.GetVideoCategory(categoryId)
|
|
if category == nil || !category.Status {
|
|
return nil, nil, srvdef.New(srvdef.ErrNoData, "", "")
|
|
}
|
|
tagConditions = append(tagConditions, bson.M{videoinformationmod.FN.VideoInformation.Tags: bson.M{"$in": category.Tags}})
|
|
}
|
|
if tagId > 0 {
|
|
tagConditions = append(tagConditions, bson.M{videoinformationmod.FN.VideoInformation.Tags: tagId})
|
|
}
|
|
switch len(tagConditions) {
|
|
case 1:
|
|
for key, value := range tagConditions[0] {
|
|
filter[key] = value
|
|
}
|
|
case 2:
|
|
filter["$and"] = tagConditions
|
|
}
|
|
switch proto.VideoRecommendSort(sortList) {
|
|
case proto.VideoRecommendSortNewest:
|
|
return filter, bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}, nil
|
|
case proto.VideoRecommendSortHot:
|
|
return filter, bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}, nil
|
|
default:
|
|
if categoryId <= 0 {
|
|
filter[videoinformationmod.FN.VideoInformation.IsRecommend] = true
|
|
} else {
|
|
return filter, videoinformationmod.CategoryRankSort(), nil
|
|
}
|
|
return filter, bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WeekWatchs, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// findVideoRecommendPage 查询当前混排分页需要的视频数据
|
|
func findVideoRecommendPage(ctx context.Context, filter bson.M, sort bson.D, skip, limit int64) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
|
|
return videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
|
|
SetProjection(queryVideoProjection()).
|
|
SetSort(sort).
|
|
SetSkip(skip).
|
|
SetLimit(limit))
|
|
}
|
|
|
|
// findVideoCategoryPage 仅在分类自定义排序时计算当前分类排序值
|
|
func findVideoCategoryPage(ctx context.Context, filter bson.M, sort bson.D, categoryId, skip, limit int64) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
|
|
if categoryId > 0 && len(sort) > 0 && sort[0].Key == videoinformationmod.CategoryRankField {
|
|
return videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, categoryId, skip, limit, queryVideoProjection())
|
|
}
|
|
return findVideoRecommendPage(ctx, filter, sort, skip, limit)
|
|
}
|
|
|
|
// videoRecommendListWithoutAds 无广告时直接按推荐视频分页查询
|
|
func videoRecommendListWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, cdn string) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
|
|
list, err := findVideoCategoryPage(ctx, filter, sort, categoryId, (pageNum-1)*pageSize, pageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]*proto.VideoRecommendListItem, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
items = append(items, videoToRecommendListItem(item, cdn))
|
|
}
|
|
return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: totalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}}, nil
|
|
}
|
|
|
|
// queryVideoMoreWithoutAds 无广告时直接按视频分页查询
|
|
func queryVideoMoreWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, cdn string) (*proto.QueryVideoResp, *srvdef.SrvErr) {
|
|
skip := (pageNum - 1) * pageSize
|
|
list, err := findVideoCategoryPage(ctx, filter, sort, categoryId, skip, pageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := make([]*proto.QueryVideoListItem, 0, len(list))
|
|
for _, item := range list {
|
|
resp = append(resp, queryVideoToListItem(item, cdn))
|
|
}
|
|
return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: totalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: resp,
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
|
|
}
|
|
|
|
// buildQueryVideoMixedList 按混排后的全局位置切出当前页视频和广告数据
|
|
func buildQueryVideoMixedList(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.QueryVideoListItem {
|
|
resp := make([]*proto.QueryVideoListItem, 0, end-start)
|
|
contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
|
|
for i, item := range list {
|
|
contentByIndex[contentStart+int64(i)] = item
|
|
}
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, queryVideoAdInterval)
|
|
if adIndex >= 0 && adIndex < adsTotal {
|
|
resp = append(resp, queryVideoAdToListItem(ads[adIndex], cdn))
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, queryVideoAdInterval)
|
|
if item := contentByIndex[contentIndex]; item != nil {
|
|
resp = append(resp, queryVideoToListItem(item, cdn))
|
|
}
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// buildVideoRecommendMixedList 按混排后的全局位置切出当前页视频和广告数据
|
|
func buildVideoRecommendMixedList(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.VideoRecommendListItem {
|
|
items := make([]*proto.VideoRecommendListItem, 0, end-start)
|
|
contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
|
|
for i, item := range list {
|
|
contentByIndex[contentStart+int64(i)] = item
|
|
}
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, videoRecommendAdInterval)
|
|
if adIndex >= 0 && adIndex < adsTotal {
|
|
items = append(items, videoAdToRecommendListItem(ads[adIndex], cdn))
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, videoRecommendAdInterval)
|
|
if item := contentByIndex[contentIndex]; item != nil {
|
|
items = append(items, videoToRecommendListItem(item, cdn))
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
// queryVideoToListItem 将视频模型转换为视频更多列表项
|
|
func queryVideoToListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.QueryVideoListItem {
|
|
return &proto.QueryVideoListItem{
|
|
Type: proto.QueryVideoItemTypeVideo,
|
|
VideoInfo: homeVideoInfo(item, cdn),
|
|
}
|
|
}
|
|
|
|
// videoToRecommendListItem 将视频模型转换为 app 推荐视频列表项
|
|
func videoToRecommendListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.VideoRecommendListItem {
|
|
return &proto.VideoRecommendListItem{
|
|
Type: proto.VideoRecommendItemTypeVideo,
|
|
VideoInfo: homeVideoInfo(item, cdn),
|
|
}
|
|
}
|
|
|
|
// favoriteVideoProjection 返回收藏视频列表需要的最小字段,减少 MongoDB 网络传输和反序列化开销
|
|
func favoriteVideoProjection() bson.M {
|
|
return bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
|
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
|
videoinformationmod.FN.VideoInformation.Tags: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
}
|
|
}
|
|
|
|
// favoriteVideoListItem 将视频模型转换为当前用户收藏列表项
|
|
func favoriteVideoListItem(item *videoinformationmod.VideoInformation, imgCdn string) *proto.FavoriteVideoListItem {
|
|
tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
|
|
return &proto.FavoriteVideoListItem{
|
|
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,
|
|
}
|
|
}
|
|
|
|
type videoHistoryRow struct {
|
|
UpdatedAt time.Time `bson:"updatedAt"`
|
|
Video videoinformationmod.VideoInformation `bson:"video"`
|
|
}
|
|
|
|
// ReportVideoHistory 校验上架视频后写入当前用户观看历史
|
|
func ReportVideoHistory(ctx context.Context, userID int64, req *proto.VideoHistoryReportReq) (*proto.VideoHistoryReportResp, *srvdef.SrvErr) {
|
|
if userID <= 0 || req == nil || req.Id <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
|
|
}
|
|
video, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: req.Id,
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if video.Video.Url == "" && video.Video.H265Url == "" {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "视频不可播放", "")
|
|
}
|
|
now := time.Now().UTC()
|
|
if err := models.TouchWatchHistory(ctx, models.WatchHistory{
|
|
UserID: userID, DataType: models.WatchHistoryTypeVideo,
|
|
ContentID: req.Id, CreatedAt: now, UpdatedAt: now,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.VideoHistoryReportResp{WatchedAt: now}, nil
|
|
}
|
|
|
|
// QueryVideoHistoryList 按最近观看时间倒序分页返回当前用户的上架视频
|
|
func QueryVideoHistoryList(ctx context.Context, userID int64, req *proto.VideoHistoryListReq) (*proto.VideoHistoryListResp, *srvdef.SrvErr) {
|
|
if userID <= 0 || req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
|
|
base := []bson.M{
|
|
{"$match": bson.M{models.WatchHistoryUserID: userID, models.WatchHistoryDataType: models.WatchHistoryTypeVideo, models.WatchHistoryUpdatedAt: bson.M{"$gte": time.Now().UTC().AddDate(0, 0, -models.WatchHistoryRetentionDays)}}},
|
|
{"$lookup": bson.M{"from": videoinformationmod.TN.VideoInformation, "localField": models.WatchHistoryContentID, "foreignField": videoinformationmod.FN.VideoInformation.Id, "as": "video"}},
|
|
{"$unwind": "$video"},
|
|
{"$match": bson.M{"video." + videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
|
|
}
|
|
countRows := make([]struct {
|
|
Total int64 `bson:"total"`
|
|
}, 0)
|
|
if err := models.AggregateWatchHistory(ctx, &countRows, append(append([]bson.M(nil), base...), bson.M{"$count": "total"})); err != nil {
|
|
return nil, err
|
|
}
|
|
total := int64(0)
|
|
if len(countRows) > 0 {
|
|
total = countRows[0].Total
|
|
}
|
|
resp := &proto.VideoHistoryListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.VideoHistoryItem, 0),
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
|
|
if total == 0 || pageNum > (total-1)/pageSize+1 {
|
|
return resp, nil
|
|
}
|
|
sortStage := bson.M{"$sort": bson.D{{Key: models.WatchHistoryUpdatedAt, Value: -1}, {Key: "_id", Value: -1}}}
|
|
pipeline := append([]bson.M{base[0], sortStage}, base[1:]...)
|
|
pipeline = append(pipeline, bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize})
|
|
rows := make([]videoHistoryRow, 0)
|
|
if err := models.AggregateWatchHistory(ctx, &rows, pipeline); err != nil {
|
|
return nil, err
|
|
}
|
|
cdn := configser.GetRequestConfig(ctx).ImgCdn
|
|
items := make([]*proto.VideoHistoryItem, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, &proto.VideoHistoryItem{
|
|
FavoriteVideoListItem: *favoriteVideoListItem(&row.Video, cdn),
|
|
Desc: row.Video.Desc, WatchedAt: row.UpdatedAt,
|
|
})
|
|
}
|
|
resp.List = items
|
|
return resp, nil
|
|
}
|
|
|
|
// ClearVideoHistory 清空当前用户的视频观看历史
|
|
func ClearVideoHistory(ctx context.Context, userID int64) (*proto.VideoHistoryClearResp, *srvdef.SrvErr) {
|
|
if userID <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "用户无效", "")
|
|
}
|
|
count, err := models.DeleteWatchHistory(ctx, userID, models.WatchHistoryTypeVideo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.VideoHistoryClearResp{DeletedCount: count}, nil
|
|
}
|
|
|
|
// normalizeQueryVideoPage 规范化视频分页参数,限制单次请求数量避免大分页拖垮 MongoDB
|
|
func normalizeQueryVideoPage(pageNum, pageSize int64) (int64, int64) {
|
|
if pageNum <= 0 {
|
|
pageNum = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = queryVideoDefaultPageSize
|
|
}
|
|
if pageSize > queryVideoMaxPageSize {
|
|
pageSize = queryVideoMaxPageSize
|
|
}
|
|
return pageNum, pageSize
|
|
}
|
|
|
|
// emptyFavoriteVideoPage 构造当前用户收藏视频空分页响应
|
|
func emptyFavoriteVideoPage(pageNum, pageSize int64) *proto.QueryFavoriteVideoListResp {
|
|
return &proto.QueryFavoriteVideoListResp{BasePageResp: response.BasePageResp{
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: make([]*proto.FavoriteVideoListItem, 0),
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
|
|
}
|
|
|
|
// queryVideoAdToListItem 将广告转换为视频更多列表项
|
|
func queryVideoAdToListItem(ad proxy.AdvitiseInfo, cdn string) *proto.QueryVideoListItem {
|
|
return &proto.QueryVideoListItem{
|
|
Type: proto.QueryVideoItemTypeAdvertise,
|
|
AdvertiseId: ad.Id,
|
|
Link: ad.Link,
|
|
Location: ad.Location,
|
|
VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
|
|
Title: ad.Title,
|
|
CoverImg: netutil.MergeURL(cdn, ad.Images),
|
|
}},
|
|
}
|
|
}
|
|
|
|
// videoAdToRecommendListItem 将广告转换为 app 推荐视频列表项
|
|
func videoAdToRecommendListItem(ad proxy.AdvitiseInfo, cdn string) *proto.VideoRecommendListItem {
|
|
return &proto.VideoRecommendListItem{
|
|
Type: proto.VideoRecommendItemTypeAdvertise,
|
|
AdvertiseId: ad.Id,
|
|
Link: ad.Link,
|
|
Location: ad.Location,
|
|
VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
|
|
Title: ad.Title,
|
|
CoverImg: netutil.MergeURL(cdn, ad.Images),
|
|
}},
|
|
}
|
|
}
|
|
|
|
// emptyQueryVideoMorePage 构造视频更多空分页响应
|
|
func emptyQueryVideoMorePage(pageNum, pageSize int64, categoryInfo proto.VideoCategoryInfo) *proto.QueryVideoResp {
|
|
return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: make([]*proto.QueryVideoListItem, 0),
|
|
}, CategoryInfo: categoryInfo, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
|
|
}
|
|
|
|
func videoCategoryInfo(categoryID int64) proto.VideoCategoryInfo {
|
|
return toVideoCategoryInfo(translationser.GetVideoCategory(categoryID))
|
|
}
|
|
|
|
func toVideoCategoryInfo(category *videocategorymod.VideoCategory) proto.VideoCategoryInfo {
|
|
if category == nil {
|
|
return proto.VideoCategoryInfo{}
|
|
}
|
|
return proto.VideoCategoryInfo{
|
|
Id: category.Id,
|
|
Name: category.Name,
|
|
Slug: category.Slug,
|
|
Title: category.Title,
|
|
Desc: category.Desc,
|
|
SeoInfo: category.SeoInfo,
|
|
}
|
|
}
|
|
|
|
// emptyVideoRecommendListPage 构造推荐视频空分页响应
|
|
func emptyVideoRecommendListPage(pageNum, pageSize int64) *proto.VideoRecommendListResp {
|
|
return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: make([]*proto.VideoRecommendListItem, 0),
|
|
}}
|
|
}
|
|
|
|
// videoRequestPath 从上下文中提取当前请求路径,避免API层重复组装视频首页响应字段
|
|
func videoRequestPath(ctx context.Context) string {
|
|
ginCtx, ok := ctx.(*gin.Context)
|
|
if !ok || ginCtx.Request == nil || ginCtx.Request.URL == nil {
|
|
return ""
|
|
}
|
|
return ginCtx.Request.URL.Path
|
|
}
|