Files
2026-10-26 16:28:14 +08:00

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
}