package videoser import ( "context" "regexp" "strings" "videoweb/app/api/proto" "videoweb/app/service/configser" "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, } 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, } 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 }