Files
web_hgdjc_server/app/service/videoser/video.go
T
2026-11-07 10:46:18 +08:00

1400 lines
48 KiB
Go

package videoser
import (
"context"
"regexp"
"strings"
"videoweb/app/api/proto"
"videoweb/app/service/configser"
"videoweb/app/service/reportser"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/utils"
"videoweb/pub/models/richtextcontentmod"
"videoweb/pub/models/videocollectmod"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/models/videolikemod"
"videoweb/pub/models/videotagmod"
"videoweb/pub/models/videotopicmod"
"videoweb/pub/models/videowatchhistorymod"
"videoweb/pub/pubdef"
"videoweb/pub/seo/h5path"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
videoListDefaultPageSize int64 = 10
videoMaxPageSize int64 = 100
videoAllTypeAll int64 = 0
videoAllTypeCollection int64 = 1
videoAllTypeAnime int64 = 2
videoAllSortLatest int64 = 1
videoAllSortHot int64 = 2
videoAllSortCollect int64 = 3
videoOperateStatusYes int64 = 1
videoOperateStatusNo int64 = 2
// videoH5DetailPath 视频播放页路径,供SEO canonical使用
videoH5DetailPath = "/series-play"
)
var (
// videoListProjection 只读取列表需要展示的字段,降低MongoDB IO和网络传输
videoListProjection = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CreatorId, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CreatorName, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Keyworld, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
}
videoAllListProjection = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.MediaType, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CreatorId, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CreatorName, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Keyworld, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.TopicIds, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.VideoCount, Value: 1},
}
// videoListSort 按上架时间和ID倒序,保证普通分页排序稳定
videoListSort = bson.D{
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
}
videoListHint = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Status, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
}
// videoInfoProjection 只读取视频详情接口需要展示和签名的字段
videoInfoProjection = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Keyworld, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Desc, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.TopicIds, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Collects, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Comments, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.TaskId, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.FileId, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.Url, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.H265Url, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.Cover, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.Height, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.Width, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4, Value: 1},
}
videoOperateProjection = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Collects, Value: 1},
}
videoUserListProjection = bson.D{
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Keyworld, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Collects, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Real.Comments, Value: 1},
{Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
}
)
type CategoryDataStruct struct {
VideoInfoData []proto.VideoInfoData
CategoryData proto.VideoCategoryInfo
VideoCount int64
}
type PublishDataStruct struct {
VideoInfoData []proto.VideoInfoData
CreaterData proto.VideoCreaterInfo
VideoCount int64
}
// VideoAllList 分页查询app端视频列表
func VideoAllList(ctx context.Context, req *proto.VideoAllListReq) (*proto.VideoAllListResp, *srvdef.SrvErr) {
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := normalizeVideoPage(req.PageNum, req.PageSize, videoListDefaultPageSize)
filter, err := videoAllListFilter(ctx, req)
if err != nil {
return nil, err
}
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
if err != nil {
return nil, err
}
resp := &proto.VideoAllListResp{
TotalCount: totalCount,
PageSize: int(pageSize),
PageNum: int(pageNum),
List: make([]*proto.VideoAllListItemResp, 0),
Advitise: configser.GetAdvitise(),
}
if totalCount == 0 {
return resp, nil
}
skip := (pageNum - 1) * pageSize
if skip >= totalCount {
return resp, nil
}
list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
SetProjection(videoAllListProjection).
SetSort(videoAllListSort(req.SortType)).
SetSkip(skip).
SetLimit(pageSize))
if err != nil {
return nil, err
}
topicMap, err := buildVideoTopicMap(ctx, list)
if err != nil {
return nil, err
}
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, item := range list {
resp.List = append(resp.List, videoToAllListResp(item, imgCdn, topicMap))
}
return resp, nil
}
// VideoList 按上架时间倒序普通分页查询已上架视频
func VideoList(ctx context.Context, req *proto.VideoReq) (*proto.VideoResp, *srvdef.SrvErr) {
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := normalizeVideoPage(req.PageNum, req.PageSize, videoListDefaultPageSize)
filter := bson.M{
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter, options.Count().SetHint(videoListHint))
if err != nil {
return nil, err
}
if totalCount == 0 {
return emptyVideoPage(pageNum, pageSize), nil
}
skip := (pageNum - 1) * pageSize
if skip >= totalCount {
resp := emptyVideoPage(pageNum, pageSize)
resp.TotalCount = totalCount
return resp, nil
}
list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
SetProjection(videoListProjection).
SetSort(videoListSort).
SetHint(videoListHint).
SetSkip(skip).
SetLimit(pageSize))
if err != nil {
return nil, err
}
items := make([]*proto.VideoItemResp, 0, len(list))
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, item := range list {
items = append(items, videoToListResp(item, imgCdn))
}
return &proto.VideoResp{
TotalCount: totalCount,
PageSize: int(pageSize),
PageNum: int(pageNum),
List: items,
}, nil
}
// SetLike 设置当前用户视频点赞状态
func SetLike(ctx context.Context, userId int64, req *proto.VideoLikeSetReq) (*proto.VideoLikeSetResp, *srvdef.SrvErr) {
// 使用指针布尔值校验是否传参,保证 false 能表示取消点赞
if req == nil || req.Id <= 0 || req.IsLike == nil {
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
}
video, changed, err := setVideoLike(ctx, userId, req.Id, *req.IsLike)
if err != nil {
return nil, err
}
countLike := applyVideoLocalCounter(video.Real.Likes, *req.IsLike, changed)
resp := &proto.VideoLikeSetResp{
Id: req.Id,
IsLike: videoOperateStatusByFlag(*req.IsLike),
CountLike: countLike,
}
if changed {
reportser.ReportVideoLike(ctx, userId, req.Id, resp.IsLike)
}
return resp, nil
}
// SetCollect 设置当前用户视频收藏状态
func SetCollect(ctx context.Context, userId int64, req *proto.VideoCollectSetReq) (*proto.VideoCollectSetResp, *srvdef.SrvErr) {
// 使用指针布尔值校验是否传参,保证 false 能表示取消收藏
if req == nil || req.Id <= 0 || req.IsCollect == nil {
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
}
video, changed, err := setVideoCollect(ctx, userId, req.Id, *req.IsCollect)
if err != nil {
return nil, err
}
countCollect := applyVideoLocalCounter(video.Real.Collects, *req.IsCollect, changed)
resp := &proto.VideoCollectSetResp{
Id: req.Id,
IsCollect: videoOperateStatusByFlag(*req.IsCollect),
CountCollect: countCollect,
}
if changed {
reportser.ReportVideoCollect(ctx, userId, req.Id, resp.IsCollect)
}
return resp, nil
}
// VideoInfo 查询视频详情
func VideoInfo(ctx context.Context, req *proto.VideoInfoReq, userId int64, ip, ua string) (*proto.VideoInfoResp, *srvdef.SrvErr) {
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
videoID, err := resolveVideoID(ctx, req.Id, req.Slug, true)
if err != nil {
return nil, err
}
req.Id = videoID
filter := bson.M{
videoinformationmod.FN.VideoInformation.Id: req.Id,
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
item, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx, filter, options.FindOne().
SetProjection(videoInfoProjection))
if err != nil {
return nil, err
}
go IncWatchTimes(context.Background(), item.Id)
imgCdn := configser.GetAppImageCdn(ip)
resp := videoToInfoResp(item, imgCdn, ip, ua, true, userId)
topics, topicErr := queryVideoTopicsByIds(ctx, item.TopicIds)
if topicErr != nil {
return nil, topicErr
}
resp.Topics = topics
isLike, isCollect, statusErr := findVideoUserOperateStatus(ctx, userId, item.Id)
if statusErr != nil {
return nil, statusErr
}
resp.IsLike = isLike
resp.IsCollect = isCollect
resp.Advitise = configser.GetAdvitise()
resp.SeoInfo = buildVideoDetailSeoInfo(item)
recordVideoWatchHistory(ctx, userId, item.Id)
return resp, nil
}
// HistoryList 分页查询当前用户的视频观看历史
func HistoryList(ctx context.Context, userId int64, req *proto.VideoHistoryListReq) (*proto.VideoHistoryListResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := normalizeVideoPage(req.PageNum, req.PageSize, videoListDefaultPageSize)
filter := bson.M{
videowatchhistorymod.FN.VideoWatchHistory.UserId: userId,
}
totalCount, err := videowatchhistorymod.CountVideoWatchHistory(ctx, filter)
if err != nil {
return nil, err
}
resp := &proto.VideoHistoryListResp{
TotalCount: totalCount,
PageSize: int(pageSize),
PageNum: int(pageNum),
List: make([]proto.VideoHistoryData, 0),
}
if totalCount == 0 {
return resp, nil
}
skip := (pageNum - 1) * pageSize
if skip >= totalCount {
return resp, nil
}
historyList, err := videowatchhistorymod.FindManyVideoWatchHistory(ctx, filter, options.Find().
SetProjection(bson.D{
{Key: videowatchhistorymod.FN.VideoWatchHistory.Id, Value: 1},
{Key: videowatchhistorymod.FN.VideoWatchHistory.VideoId, Value: 1},
{Key: videowatchhistorymod.FN.VideoWatchHistory.UpdatedAt, Value: 1},
}).
SetSort(bson.D{
{Key: videowatchhistorymod.FN.VideoWatchHistory.UpdatedAt, Value: -1},
{Key: videowatchhistorymod.FN.VideoWatchHistory.Id, Value: -1},
}).
SetSkip(skip).
SetLimit(pageSize))
if err != nil {
return nil, err
}
videoIds := collectVideoIdsFromHistories(historyList)
videoMap, err := findUserVideoMap(ctx, videoIds)
if err != nil {
return nil, err
}
collectMap, err := findUserCollectedVideoMap(ctx, userId, videoIds)
if err != nil {
return nil, err
}
resp.List = buildVideoHistoryData(ctx, historyList, videoMap, collectMap)
return resp, nil
}
// DeleteHistory 删除当前用户指定视频观看历史
func DeleteHistory(ctx context.Context, userId int64, req *proto.VideoHistoryDeleteReq) (*proto.VideoHistoryDeleteResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
if req == nil || req.VideoId <= 0 {
return nil, srvdef.New(srvdef.ErrParamError, "视频ID不能为空", "视频ID不能为空")
}
deleted, err := videowatchhistorymod.DeleteManyVideoWatchHistory(ctx, bson.M{
videowatchhistorymod.FN.VideoWatchHistory.UserId: userId,
videowatchhistorymod.FN.VideoWatchHistory.VideoId: req.VideoId,
})
if err != nil {
log.Error("删除单条视频观看历史失败", log.Any("用户ID", userId), log.Any("视频ID", req.VideoId), log.E(err))
return nil, err
}
return &proto.VideoHistoryDeleteResp{DeletedCount: deleted}, nil
}
// ClearHistory 清空当前用户的视频观看历史
func ClearHistory(ctx context.Context, userId int64) (*proto.VideoHistoryClearResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
deleted, err := videowatchhistorymod.DeleteManyVideoWatchHistory(ctx, bson.M{
videowatchhistorymod.FN.VideoWatchHistory.UserId: userId,
})
if err != nil {
log.Error("清空视频观看历史失败", log.Any("用户ID", userId), log.E(err))
return nil, err
}
return &proto.VideoHistoryClearResp{DeletedCount: deleted}, nil
}
// CollectList 分页查询当前用户的视频收藏列表
func CollectList(ctx context.Context, userId int64, req *proto.VideoCollectListReq) (*proto.VideoCollectListResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := normalizeVideoPage(req.PageNum, req.PageSize, videoListDefaultPageSize)
filter := bson.M{
videocollectmod.FN.VideoCollect.UserId: userId,
}
totalCount, err := videocollectmod.CountVideoCollect(ctx, filter)
if err != nil {
return nil, err
}
resp := &proto.VideoCollectListResp{
TotalCount: totalCount,
PageSize: int(pageSize),
PageNum: int(pageNum),
List: make([]proto.VideoCollectData, 0),
}
if totalCount == 0 {
return resp, nil
}
skip := (pageNum - 1) * pageSize
if skip >= totalCount {
return resp, nil
}
collectList, err := videocollectmod.FindManyVideoCollect(ctx, filter, options.Find().
SetProjection(bson.D{
{Key: videocollectmod.FN.VideoCollect.Id, Value: 1},
{Key: videocollectmod.FN.VideoCollect.VideoId, Value: 1},
{Key: videocollectmod.FN.VideoCollect.CreatedAt, Value: 1},
}).
SetSort(bson.D{
{Key: videocollectmod.FN.VideoCollect.CreatedAt, Value: -1},
{Key: videocollectmod.FN.VideoCollect.Id, Value: -1},
}).
SetSkip(skip).
SetLimit(pageSize))
if err != nil {
return nil, err
}
videoIds := collectVideoIdsFromCollects(collectList)
videoMap, err := findUserVideoMap(ctx, videoIds)
if err != nil {
return nil, err
}
resp.List = buildVideoCollectData(ctx, collectList, videoMap)
return resp, nil
}
// setVideoLike 设置视频点赞关系
func setVideoLike(ctx context.Context, userId int64, videoId int64, enabled bool) (*videoinformationmod.VideoInformation, bool, *srvdef.SrvErr) {
// 操作接口必须登录,先查询视频确保目标存在且已上架,再按点赞表真实变更决定是否更新点赞数
if userId <= 0 {
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
video, err := findVideoOperateTarget(ctx, userId, videoId)
if err != nil {
return nil, false, err
}
filter := bson.M{
videolikemod.FN.VideoLike.UserId: userId,
videolikemod.FN.VideoLike.VideoId: videoId,
}
changed := false
if enabled {
inserted, err := videolikemod.InsertOneVideoLike(ctx, &videolikemod.VideoLike{
UserId: userId,
VideoId: videoId,
})
if err != nil {
log.Error("新增视频点赞关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return nil, false, err
}
changed = inserted
} else {
deleted, err := videolikemod.DeleteOneVideoLike(ctx, filter)
if err != nil {
log.Error("删除视频点赞关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return nil, false, err
}
changed = deleted > 0
}
if changed {
delta := int64(1)
if !enabled {
delta = -1
}
if err := incrementVideoOperateCounter(ctx, videoId, videoinformationmod.FN.VideoInformation.Real.Likes, delta); err != nil {
rollbackVideoLike(ctx, userId, videoId, enabled)
return nil, false, err
}
}
return video, changed, nil
}
// setVideoCollect 设置视频收藏关系
func setVideoCollect(ctx context.Context, userId int64, videoId int64, enabled bool) (*videoinformationmod.VideoInformation, bool, *srvdef.SrvErr) {
// 操作接口必须登录,先查询视频确保目标存在且已上架,再按收藏表真实变更决定是否更新收藏数
if userId <= 0 {
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
}
video, err := findVideoOperateTarget(ctx, userId, videoId)
if err != nil {
return nil, false, err
}
filter := bson.M{
videocollectmod.FN.VideoCollect.UserId: userId,
videocollectmod.FN.VideoCollect.VideoId: videoId,
}
changed := false
if enabled {
inserted, err := videocollectmod.InsertOneVideoCollect(ctx, &videocollectmod.VideoCollect{
UserId: userId,
VideoId: videoId,
})
if err != nil {
log.Error("新增视频收藏关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return nil, false, err
}
changed = inserted
} else {
deleted, err := videocollectmod.DeleteOneVideoCollect(ctx, filter)
if err != nil {
log.Error("删除视频收藏关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return nil, false, err
}
changed = deleted > 0
}
if changed {
delta := int64(1)
if !enabled {
delta = -1
}
if err := incrementVideoOperateCounter(ctx, videoId, videoinformationmod.FN.VideoInformation.Real.Collects, delta); err != nil {
rollbackVideoCollect(ctx, userId, videoId, enabled)
return nil, false, err
}
}
return video, changed, nil
}
// findVideoOperateTarget 查询视频操作目标
func findVideoOperateTarget(ctx context.Context, userId int64, videoId int64) (*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
video, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}, options.FindOne().SetProjection(videoOperateProjection))
if err != nil {
log.Error("查询视频操作目标失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return nil, err
}
return video, nil
}
// incrementVideoOperateCounter 增减视频点赞或收藏计数
func incrementVideoOperateCounter(ctx context.Context, videoId int64, countField string, delta int64) *srvdef.SrvErr {
filter := bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
if delta < 0 {
filter[countField] = bson.M{"$gt": 0}
}
err := videoinformationmod.UpdateOneVideoInformation(ctx, filter, bson.M{
"$inc": bson.M{countField: delta},
})
if err != nil {
if delta < 0 && err.Code == srvdef.ErrNoData {
log.Warn("视频操作计数已为0,跳过扣减", log.Any("视频ID", videoId), log.Any("计数字段", countField))
return nil
}
log.Error("更新视频操作计数失败", log.Any("视频ID", videoId), log.Any("计数字段", countField), log.Any("增量", delta), log.E(err))
return err
}
return nil
}
// rollbackVideoLike 回滚视频点赞关系
func rollbackVideoLike(ctx context.Context, userId int64, videoId int64, enabled bool) {
filter := bson.M{
videolikemod.FN.VideoLike.UserId: userId,
videolikemod.FN.VideoLike.VideoId: videoId,
}
if enabled {
if _, err := videolikemod.DeleteOneVideoLike(ctx, filter); err != nil {
log.Error("回滚视频点赞关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
}
return
}
if _, err := videolikemod.InsertOneVideoLike(ctx, &videolikemod.VideoLike{
UserId: userId,
VideoId: videoId,
}); err != nil {
log.Error("恢复视频点赞关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
}
}
// rollbackVideoCollect 回滚视频收藏关系
func rollbackVideoCollect(ctx context.Context, userId int64, videoId int64, enabled bool) {
filter := bson.M{
videocollectmod.FN.VideoCollect.UserId: userId,
videocollectmod.FN.VideoCollect.VideoId: videoId,
}
if enabled {
if _, err := videocollectmod.DeleteOneVideoCollect(ctx, filter); err != nil {
log.Error("回滚视频收藏关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
}
return
}
if _, err := videocollectmod.InsertOneVideoCollect(ctx, &videocollectmod.VideoCollect{
UserId: userId,
VideoId: videoId,
}); err != nil {
log.Error("恢复视频收藏关系失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
}
}
// findVideoUserOperateStatus 查询当前用户视频点赞和收藏状态
func findVideoUserOperateStatus(ctx context.Context, userId int64, videoId int64) (int64, int64, *srvdef.SrvErr) {
if userId <= 0 {
return videoOperateStatusNo, videoOperateStatusNo, nil
}
likeExists, err := videolikemod.ExistsVideoLike(ctx, bson.M{
videolikemod.FN.VideoLike.UserId: userId,
videolikemod.FN.VideoLike.VideoId: videoId,
})
if err != nil {
log.Error("查询视频用户点赞状态失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return 0, 0, err
}
collectExists, err := videocollectmod.ExistsVideoCollect(ctx, bson.M{
videocollectmod.FN.VideoCollect.UserId: userId,
videocollectmod.FN.VideoCollect.VideoId: videoId,
})
if err != nil {
log.Error("查询视频用户收藏状态失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
return 0, 0, err
}
return videoOperateStatusByFlag(likeExists), videoOperateStatusByFlag(collectExists), nil
}
// applyVideoLocalCounter 根据本次关系变更结果计算响应计数
func applyVideoLocalCounter(current int64, enabled bool, changed bool) int64 {
if !changed {
return current
}
if enabled {
return current + 1
}
if current <= 0 {
return 0
}
return current - 1
}
// videoOperateStatusByFlag 转换视频操作状态响应值
func videoOperateStatusByFlag(enabled bool) int64 {
if enabled {
return videoOperateStatusYes
}
return videoOperateStatusNo
}
// normalizeVideoPage 规范化视频普通分页参数,限制单次查询量保护接口和数据库
func normalizeVideoPage(pageNum, pageSize, defaultPageSize int64) (int64, int64) {
if pageNum <= 0 {
pageNum = 1
}
if pageSize <= 0 {
pageSize = defaultPageSize
}
if pageSize > videoMaxPageSize {
pageSize = videoMaxPageSize
}
return pageNum, pageSize
}
func emptyVideoPage(pageNum, pageSize int64) *proto.VideoResp {
return &proto.VideoResp{
PageSize: int(pageSize),
PageNum: int(pageNum),
List: make([]*proto.VideoItemResp, 0),
}
}
// videoToListResp 将视频模型转换为列表项
func videoToListResp(item *videoinformationmod.VideoInformation, imgCdn string) *proto.VideoItemResp {
return &proto.VideoItemResp{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
AddedTime: item.AddedTime,
CreatorId: item.CreatorId,
CreatorName: item.CreatorName,
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
Keyworld: item.Keyworld,
Tags: item.Tags,
WatchTimes: item.Real.WatchTimes + item.Fake.WatchTimes,
VideoTime: item.Video.VideoTime,
}
}
// videoToAllListResp 将视频模型转换为全部视频列表项
func videoToAllListResp(item *videoinformationmod.VideoInformation, imgCdn string, topicMap map[int64]proto.VideoTopicInfo) *proto.VideoAllListItemResp {
resp := &proto.VideoAllListItemResp{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
MediaType: appVideoMediaType(item.MediaType),
AddedTime: item.AddedTime,
CreatorId: item.CreatorId,
CreatorName: item.CreatorName,
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
Keyworld: item.Keyworld,
Tags: item.Tags,
TopicIds: item.TopicIds,
TopicNames: make([]string, 0, len(item.TopicIds)),
Topics: make([]proto.VideoTopicInfo, 0, len(item.TopicIds)),
WatchTimes: item.Real.WatchTimes + item.Fake.WatchTimes,
VideoTime: item.Video.VideoTime,
VideoCount: item.VideoCount,
}
for _, id := range item.TopicIds {
topic, ok := topicMap[id]
if !ok {
continue
}
resp.TopicNames = append(resp.TopicNames, topic.Name)
resp.Topics = append(resp.Topics, topic)
}
return resp
}
// videoToInfoResp 将视频模型转换为详情响应,并给视频播放地址增加鉴权参数
func videoToInfoResp(item *videoinformationmod.VideoInformation, imgCdn, ip, ua string, includeCurrentWatch bool, userId int64) *proto.VideoInfoResp {
video := proto.VideoData{
TaskId: item.Video.TaskId,
FileId: item.Video.FileId,
Url: item.Video.Url,
Cover: netutil.MergeURL(imgCdn, item.Video.Cover),
Height: item.Video.Height,
Width: item.Video.Width,
VideoTime: item.Video.VideoTime,
PreMp4FileNameFs: item.Video.PreMp4FileNameFs,
UnencryptedPreMp4: item.Video.UnencryptedPreMp4,
}
if video.Url != "" {
video.Url = GetM3U8AuthParam(item.Id, video.Url, ip, ua, userId)
}
watchTimes := item.Real.WatchTimes + item.Fake.WatchTimes
if includeCurrentWatch {
watchTimes++
}
resp := &proto.VideoInfoResp{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
Keyworld: item.Keyworld,
Tags: item.Tags,
TagsName: make([]string, 0, len(item.Tags)),
TopicIds: item.TopicIds,
Topics: make([]proto.VideoTopicInfo, 0, len(item.TopicIds)),
WatchTimes: watchTimes,
CountLike: item.Real.Likes,
CountCollect: item.Real.Collects,
CountComment: item.Real.Comments,
IsLike: videoOperateStatusNo,
IsCollect: videoOperateStatusNo,
Video: video,
}
for _, tagId := range item.Tags {
tagInfo := configser.GetTag(tagId)
if tagInfo != nil {
resp.TagsName = append(resp.TagsName, tagInfo.Name)
}
}
return resp
}
// recordVideoWatchHistory 记录当前登录用户的视频观看历史
func recordVideoWatchHistory(ctx context.Context, userId int64, videoId int64) {
if userId <= 0 || videoId <= 0 {
return
}
go func() {
if err := videowatchhistorymod.SaveVideoWatchHistory(context.Background(), userId, videoId); err != nil {
log.Error("记录视频观看历史失败", log.Any("用户ID", userId), log.Any("视频ID", videoId), log.E(err))
}
}()
}
// collectVideoIdsFromHistories 按观看历史顺序收集视频ID并去重
func collectVideoIdsFromHistories(list videowatchhistorymod.VideoWatchHistoryList) []int64 {
result := make([]int64, 0, len(list))
seen := make(map[int64]struct{}, len(list))
for _, item := range list {
if item == nil || item.VideoId <= 0 {
continue
}
if _, ok := seen[item.VideoId]; ok {
continue
}
seen[item.VideoId] = struct{}{}
result = append(result, item.VideoId)
}
return result
}
// collectVideoIdsFromCollects 按收藏列表顺序收集视频ID并去重
func collectVideoIdsFromCollects(list videocollectmod.VideoCollectList) []int64 {
result := make([]int64, 0, len(list))
seen := make(map[int64]struct{}, len(list))
for _, item := range list {
if item == nil || item.VideoId <= 0 {
continue
}
if _, ok := seen[item.VideoId]; ok {
continue
}
seen[item.VideoId] = struct{}{}
result = append(result, item.VideoId)
}
return result
}
// findUserVideoMap 批量查询用户列表关联的视频数据
func findUserVideoMap(ctx context.Context, videoIds []int64) (map[int64]*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
result := make(map[int64]*videoinformationmod.VideoInformation, len(videoIds))
if len(videoIds) == 0 {
return result, nil
}
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}, options.Find().SetProjection(videoUserListProjection))
if err != nil {
log.Error("批量查询用户视频列表失败", log.Any("视频ID集合", videoIds), log.E(err))
return nil, err
}
for _, item := range list {
if item == nil {
continue
}
result[item.Id] = item
}
return result, nil
}
// findUserCollectedVideoMap 批量查询当前用户收藏的视频ID
func findUserCollectedVideoMap(ctx context.Context, userId int64, videoIds []int64) (map[int64]struct{}, *srvdef.SrvErr) {
result := make(map[int64]struct{}, len(videoIds))
if userId <= 0 || len(videoIds) == 0 {
return result, nil
}
list, err := videocollectmod.FindManyVideoCollect(ctx, bson.M{
videocollectmod.FN.VideoCollect.UserId: userId,
videocollectmod.FN.VideoCollect.VideoId: bson.M{"$in": videoIds},
}, options.Find().SetProjection(bson.D{
{Key: videocollectmod.FN.VideoCollect.VideoId, Value: 1},
}))
if err != nil {
log.Error("批量查询用户视频收藏状态失败", log.Any("用户ID", userId), log.Any("视频ID集合", videoIds), log.E(err))
return nil, err
}
for _, item := range list {
if item == nil {
continue
}
result[item.VideoId] = struct{}{}
}
return result, nil
}
// buildVideoHistoryData 组装视频观看历史响应
func buildVideoHistoryData(
ctx context.Context,
historyList videowatchhistorymod.VideoWatchHistoryList,
videoMap map[int64]*videoinformationmod.VideoInformation,
collectMap map[int64]struct{},
) []proto.VideoHistoryData {
result := make([]proto.VideoHistoryData, 0, len(historyList))
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, history := range historyList {
if history == nil {
continue
}
item := videoMap[history.VideoId]
if item == nil {
continue
}
_, collected := collectMap[item.Id]
result = append(result, proto.VideoHistoryData{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
Keyworld: item.Keyworld,
Tags: item.Tags,
WatchTimes: item.Real.WatchTimes + item.Fake.WatchTimes,
VideoTime: item.Video.VideoTime,
CountLike: item.Real.Likes,
CountCollect: item.Real.Collects,
CountComment: item.Real.Comments,
IsCollect: videoOperateStatusByFlag(collected),
HistoryAt: history.UpdatedAt,
})
}
return result
}
// buildVideoCollectData 组装视频收藏列表响应
func buildVideoCollectData(
ctx context.Context,
collectList videocollectmod.VideoCollectList,
videoMap map[int64]*videoinformationmod.VideoInformation,
) []proto.VideoCollectData {
result := make([]proto.VideoCollectData, 0, len(collectList))
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, collect := range collectList {
if collect == nil {
continue
}
item := videoMap[collect.VideoId]
if item == nil {
continue
}
result = append(result, proto.VideoCollectData{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
Keyworld: item.Keyworld,
Tags: item.Tags,
WatchTimes: item.Real.WatchTimes + item.Fake.WatchTimes,
VideoTime: item.Video.VideoTime,
CountLike: item.Real.Likes,
CountCollect: item.Real.Collects,
CountComment: item.Real.Comments,
IsCollect: videoOperateStatusYes,
CollectedAt: collect.CreatedAt,
})
}
return result
}
// queryVideoTopicsByIds 按视频题材ID集合查询题材信息
func queryVideoTopicsByIds(ctx context.Context, topicIds []int64) ([]proto.VideoTopicInfo, *srvdef.SrvErr) {
result := make([]proto.VideoTopicInfo, 0)
if len(topicIds) == 0 {
return result, nil
}
ids := make([]int64, 0, len(topicIds))
seen := make(map[int64]struct{}, len(topicIds))
for _, id := range topicIds {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
if len(ids) == 0 {
return result, nil
}
topics, err := videotopicmod.FindManyVideoTopicModels(ctx, bson.M{
videotopicmod.FN.VideoTopicModel.Id: bson.M{"$in": ids},
}, options.Find().SetProjection(bson.D{
{Key: videotopicmod.FN.VideoTopicModel.Id, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Name, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Slug, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Title, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Desc, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Image, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.VideoCategoryId, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Title, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Description, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Keyworld, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Works, Value: 1},
}))
if err != nil {
return nil, err
}
topicMap := make(map[int64]proto.VideoTopicInfo, len(topics))
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, item := range topics {
info := videoTopicToInfo(item, imgCdn)
topicMap[info.Id] = info
}
for _, id := range ids {
if topic, ok := topicMap[id]; ok {
result = append(result, topic)
}
}
return result, nil
}
// buildVideoDetailSeoInfo 组装视频详情页SEO信息
//
// canonical 统一指向Web/H5真实播放页:/series-play/{slug}
func buildVideoDetailSeoInfo(item *videoinformationmod.VideoInformation) *proto.SeoInfo {
seo := configser.GetSiteSeoInfo()
if item == nil {
return &seo
}
if title := strings.TrimSpace(item.Title); title != "" {
seo.Title = title
}
if desc := truncateVideoSEODescription(strings.TrimSpace(item.Desc), 120); desc != "" {
seo.Description = desc
}
seo.Keywords = buildVideoSEOKeywords(item, seo.Keywords)
seo.CanonicalURL = buildVideoDetailCanonicalURL(seo.CanonicalURL, item.Slug, item.Id)
return &seo
}
// buildVideoDetailCanonicalURL 拼接视频播放页标准URL
func buildVideoDetailCanonicalURL(baseURL, slug string, id int64) string {
pagePath := h5path.VideoPlay(slug, id)
if pagePath == "" {
pagePath = videoH5DetailPath
}
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if baseURL == "" {
return pagePath
}
return baseURL + pagePath
}
// buildVideoSEOKeywords 合并视频关键词和全站关键词,并去重
func buildVideoSEOKeywords(item *videoinformationmod.VideoInformation, siteKeywords []string) []string {
seen := make(map[string]struct{}, len(item.Keyworld)+len(siteKeywords)+1)
keywords := make([]string, 0, len(item.Keyworld)+len(siteKeywords)+1)
add := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if _, ok := seen[value]; ok {
return
}
seen[value] = struct{}{}
keywords = append(keywords, value)
}
add(item.Title)
for _, keyword := range item.Keyworld {
add(keyword)
}
for _, keyword := range siteKeywords {
add(keyword)
}
return keywords
}
// truncateVideoSEODescription 限制meta description长度,避免详情长文本直接写进SEO描述
func truncateVideoSEODescription(desc string, maxRunes int) string {
if maxRunes <= 0 || desc == "" {
return ""
}
runes := []rune(desc)
if len(runes) <= maxRunes {
return desc
}
return string(runes[:maxRunes])
}
func VideoDetails(ctx context.Context, videoId int64, userId int64, ip, ua string) (resp proto.VideoInfoData, serr *srvdef.SrvErr) {
imgCdn := configser.GetAppImageCdn(ip)
videoFilter := bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, videoFilter)
if serr != nil {
log.Error("VideoDetails", log.Any("videoFilter", videoFilter), log.E(serr))
return
}
// 查询分类
resp = toVideoInfoData(videoInfo, imgCdn)
for _, tagId := range videoInfo.Tags {
tagInfo := configser.GetTag(tagId)
if tagInfo != nil {
resp.Tags = append(resp.Tags, proto.TagsInfoData{
Id: tagInfo.Id,
Name: tagInfo.Name,
Slug: tagInfo.Slug,
})
}
}
if resp.WatchTimes > 0 {
resp.WatchTimes = resp.WatchTimes * 100
} else {
resp.WatchTimes = utils.GetRandom(2000, 10000)
}
if resp.Content != "" {
re := regexp.MustCompile(`data-src="([^"]+?\.m3u8)"`)
// 查找所有匹配项
matches := re.FindAllStringSubmatch(resp.Content, -1)
for _, match := range matches {
newUrl := GetM3U8AuthParam(videoId, match[1], ip, ua, userId)
resp.Content = strings.ReplaceAll(resp.Content, match[1], newUrl)
}
}
if len(resp.Video.Url) > 0 || len(resp.Nodes) > 0 {
applyVideoUrlAuth(&resp, videoId, ip, ua, userId)
}
go func() {
_ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.ShowTimes: 1}})
if len(videoInfo.Tags) > 0 {
tagIds := videoInfo.Tags
if len(tagIds) > 10 {
tagIds = tagIds[:10]
}
_ = videotagmod.UpdateManyVideoTagInfo(ctx, bson.M{
videotagmod.FN.VideoTagInfo.Id: bson.M{"$in": tagIds},
}, bson.M{"$inc": bson.M{
videotagmod.FN.VideoTagInfo.WatchTimes: 1,
}})
}
}()
return resp, nil
}
func RichtextData(ctx context.Context, richtextId int64) (proto.RichTextInfo, *srvdef.SrvErr) {
//cacheKey := fmt.Sprintf("rich_text_data_%d", richtextId)
var resp *proto.RichTextInfo
//if cacheData, exists := Cache.Get(cacheKey); exists {
// if item, ok := cacheData.(proto.RichTextInfo); ok {
// resp = &item
// }
//}
//if resp == nil || resp.Id == 0 {
filter := bson.M{
richtextcontentmod.FN.RichTextContentModel.Id: richtextId,
}
richtextInfo, serr := richtextcontentmod.FindOneRichTextContentModelModel(ctx, filter)
if serr != nil {
log.Error("VideoTagData", log.Any("videoFilter", filter), log.E(serr))
return proto.RichTextInfo{}, serr
}
data := &proto.RichTextInfo{
Id: richtextInfo.Id,
Title: richtextInfo.Title,
Content: richtextInfo.Content,
Desc: richtextInfo.Desc,
SeoInfo: richtextInfo.SeoInfo,
}
resp = data
// Cache.Set(cacheKey, data, 17*time.Minute)
//}
return *resp, nil
}
func IncWatchTimes(ctx context.Context, videoId int64) {
_ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
}, bson.M{"$inc": bson.M{
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
}})
}
// VideoTopicList 查询app端视频题材列表
func VideoTopicList(ctx context.Context, req *proto.VideoTopicListReq) (*proto.VideoTopicListResp, *srvdef.SrvErr) {
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
filter := bson.M{
videotopicmod.FN.VideoTopicModel.Status: true,
}
mediaType, err := resolveAppVideoMediaType(req.MediaType)
if err != nil {
return nil, err
}
if req.MediaType != videoAllTypeAll {
filter[videotopicmod.FN.VideoTopicModel.VideoTopicType] = mediaType
}
list, err := videotopicmod.FindManyVideoTopicModels(ctx, filter, options.Find().
SetSort(bson.D{
{Key: videotopicmod.FN.VideoTopicModel.Rank, Value: -1},
{Key: videotopicmod.FN.VideoTopicModel.Id, Value: -1},
}))
if err != nil {
return nil, err
}
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
resp := &proto.VideoTopicListResp{List: make([]proto.VideoTopicInfo, 0, len(list))}
for _, item := range list {
resp.List = append(resp.List, videoTopicToInfo(item, imgCdn))
}
return resp, nil
}
// videoAllListFilter 构建全部视频分页过滤条件
func videoAllListFilter(ctx context.Context, req *proto.VideoAllListReq) (bson.M, *srvdef.SrvErr) {
filter := bson.M{
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
mediaType, err := resolveAppVideoMediaType(req.MediaType)
if err != nil {
return nil, err
}
if req.MediaType != videoAllTypeAll {
filter[videoinformationmod.FN.VideoInformation.MediaType] = mediaType
}
topicID, serr := resolveVideoTopicID(ctx, req.TopicId, req.TopicSlug)
if serr != nil {
return nil, serr
}
if topicID > 0 {
filter[videoinformationmod.FN.VideoInformation.TopicIds] = topicID
}
return filter, nil
}
// videoAllListSort 构建全部视频分页排序
func videoAllListSort(sortType int64) bson.D {
switch sortType {
case videoAllSortHot, videoAllSortCollect:
return bson.D{
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
}
case videoAllSortLatest:
fallthrough
default:
return bson.D{
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
}
}
}
// resolveAppVideoMediaType 将app类型映射到视频表mediaType
func resolveAppVideoMediaType(mediaType int64) (videoinformationmod.MediaType, *srvdef.SrvErr) {
switch mediaType {
case videoAllTypeAll:
return 0, nil
case videoAllTypeCollection:
return videoinformationmod.MediaType_Collection, nil
case videoAllTypeAnime:
return videoinformationmod.MediaType_VideoV2, nil
default:
return 0, srvdef.New(srvdef.ErrParamError, "类型无效", "类型无效")
}
}
// appVideoMediaType 将视频表mediaType转成app类型
func appVideoMediaType(mediaType videoinformationmod.MediaType) int64 {
switch mediaType {
case videoinformationmod.MediaType_Collection:
return videoAllTypeCollection
case videoinformationmod.MediaType_VideoV2:
return videoAllTypeAnime
default:
return int64(mediaType)
}
}
// buildVideoTopicMap 批量查询视频题材
func buildVideoTopicMap(ctx context.Context, list videoinformationmod.VideoInformationList) (map[int64]proto.VideoTopicInfo, *srvdef.SrvErr) {
ids := make([]int64, 0)
seen := make(map[int64]struct{})
for _, item := range list {
if item == nil {
continue
}
for _, id := range item.TopicIds {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
}
result := make(map[int64]proto.VideoTopicInfo, len(ids))
if len(ids) == 0 {
return result, nil
}
topics, err := videotopicmod.FindManyVideoTopicModels(ctx, bson.M{
videotopicmod.FN.VideoTopicModel.Id: bson.M{"$in": ids},
}, options.Find().SetProjection(bson.D{
{Key: videotopicmod.FN.VideoTopicModel.Id, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Name, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Slug, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Title, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Desc, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Image, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.VideoCategoryId, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Title, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Description, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.SeoInfo.Keyworld, Value: 1},
{Key: videotopicmod.FN.VideoTopicModel.Works, Value: 1},
}))
if err != nil {
return nil, err
}
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
for _, item := range topics {
info := videoTopicToInfo(item, imgCdn)
result[info.Id] = info
}
return result, nil
}
// videoTopicToInfo 转换视频题材响应
func videoTopicToInfo(item *videotopicmod.VideoTopicModel, imgCdn string) proto.VideoTopicInfo {
if item == nil {
return proto.VideoTopicInfo{}
}
return proto.VideoTopicInfo{
Id: item.Id,
Name: item.Name,
Slug: item.Slug,
Title: item.Title,
Desc: item.Desc,
Image: netutil.MergeURL(imgCdn, item.Image),
VideoCategoryId: item.VideoCategoryId,
SeoInfo: item.SeoInfo,
Works: item.Works,
}
}
func VideoTopicData(ctx context.Context, isAv bool, topicId int64, sortType int64, pageNum int64) (videoCount int64, videoInfos []proto.VideoInfoData, sortItem videoinformationmod.VideoSortItem, err *srvdef.SrvErr) {
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
videoCount = int64(0)
sort := videoinformationmod.VideoSortType(sortType)
sortItem = sort.GetSortItem()
videoInfos = make([]proto.VideoInfoData, 0)
filter := bson.M{
videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_VideoV2,
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
}
opts := &options.FindOptions{}
opts.SetLimit(pubdef.PageSize).SetSkip((pageNum - 1) * pubdef.PageSize).SetSort(sort.GetSortBson())
videoCount, _ = videoinformationmod.CountVideoInformations(ctx, filter)
if videoCount == 0 {
return
}
videoList, err := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
if err != nil {
log.Warn("VideoTopicData FindManyVideoInfomations", log.Any("filter", filter), log.E(err))
return
}
for i := 0; i < len(videoList); i++ {
info := toVideoInfoData(videoList[i], imgCdn)
for _, tid := range videoList[i].Tags {
tagInfo := configser.GetTag(tid)
if tagInfo != nil {
info.Tags = append(info.Tags, proto.TagsInfoData{
Id: tagInfo.Id,
Name: tagInfo.Name,
Slug: tagInfo.Slug,
})
}
}
if info.WatchTimes > 0 {
info.WatchTimes = info.WatchTimes * 100
} else {
info.WatchTimes = utils.GetRandom(2000, 10000)
}
videoInfos = append(videoInfos, info)
}
return
}