package srv_videoinfo import ( "context" "math/rand" "net/http" "time" "videoweb/pub/common/godef" "videoweb/pub/common/log" "videoweb/pub/common/playletcache" "videoweb/pub/common/slugx" "videoweb/pub/common/srvdef" "videoweb/pub/common/utils" "videoweb/pub/models/actress" "videoweb/pub/models/contenttagmod" "videoweb/pub/models/videocategorymod" "videoweb/pub/models/videocollectionmediamod" "videoweb/pub/models/videocreatermod" "videoweb/pub/models/videoinformationmod" "videoweb/pub/mongos" "videoweb/pub/pubdef" "videoweb/pub/storage" "videoweb/pub/storage/proxy" "videoweb/web/service/srv_contenttag" "videoweb/web/service/srv_report" "videoweb/web/service/srv_seopush" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) // GetVideoInfos 按照参数搜索APP func GetVideoInfos(ctx context.Context, param *VideoInfoReq) (*VideoInfoResp, *srvdef.SrvErr) { filter := bson.M{} if param.MediaType != nil { filter[videoinformationmod.FN.VideoInformation.MediaType] = *param.MediaType } if param.Id != nil && *param.Id > 0 { filter[videoinformationmod.FN.VideoInformation.Id] = *param.Id } if param.Title != nil && len(*param.Title) > 0 { filter[videoinformationmod.FN.VideoInformation.Title] = bson.M{"$regex": *param.Title} } if param.Bango != nil && *param.Bango != "" { filter[videoinformationmod.FN.VideoInformation.Bango] = bson.M{"$regex": *param.Bango} } if param.CreatorId != nil && *param.CreatorId > 0 { filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId } if param.Status != nil { filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status } if param.Tag != nil && *param.Tag > 0 { filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": []int64{*param.Tag}} } if param.IsHot != nil { filter[videoinformationmod.FN.VideoInformation.IsHot] = *param.IsHot } if param.IsRecommend != nil { filter[videoinformationmod.FN.VideoInformation.IsRecommend] = *param.IsRecommend } if param.CreatorName != nil && len(*param.CreatorName) > 0 { filter[videoinformationmod.FN.VideoInformation.CreatorName] = bson.M{"$regex": *param.CreatorName} } if param.Keyworld != nil && len(*param.Keyworld) > 0 { filter[videoinformationmod.FN.VideoInformation.Keyworld] = bson.M{"$in": []string{*param.Keyworld}} } if param.VideoTaskId != nil && len(*param.VideoTaskId) > 0 { filter[videoinformationmod.FN.VideoInformation.Video.TaskId] = *param.VideoTaskId } if param.VideoFileId != nil && len(*param.VideoFileId) > 0 { filter[videoinformationmod.FN.VideoInformation.Video.FileId] = *param.VideoFileId } if param.CategoryId != nil && *param.CategoryId > 0 { filter[videoinformationmod.FN.VideoInformation.CategoryId] = *param.CategoryId } if param.IsTop != nil { filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop } if param.ActressIds != nil && len(*param.ActressIds) > 0 { filter[videoinformationmod.FN.VideoInformation.ActressIds] = bson.M{"$in": *param.ActressIds} } if param.StartTime != nil && param.EndTime != nil { start := utils.GetDayBeginByTime(*param.StartTime) end := utils.GetDayEndByTime(*param.EndTime) if end.Unix() > start.Unix() { filter[videoinformationmod.FN.VideoInformation.CreatedAt] = bson.M{ "$gte": start, "$lte": end, } } } if param.OptionAccount != nil { filter[videoinformationmod.FN.VideoInformation.OptionAccount] = *param.OptionAccount } opt := &options.FindOptions{} resp := &VideoInfoResp{ Total: 0, List: make(videoinformationmod.VideoInformationList, 0), } opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1}) switch param.Sort { case 0: opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.CreatedAt: -1}) case 1: opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1}) case 2: opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1}) case 3: opt.SetSort(bson.D{ {videoinformationmod.FN.VideoInformation.Sort, -1}, {videoinformationmod.FN.VideoInformation.AddedTime, -1}, }) case 4: opt.SetSort(bson.D{ {videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1}, {videoinformationmod.FN.VideoInformation.AddedTime, -1}, }) } opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize) total, serr := videoinformationmod.CountVideoInformations(ctx, filter) if serr != nil { return resp, serr } resp.Total = total if total == 0 { return resp, nil } var ads videoinformationmod.VideoInformationList if param.Sort == 3 && param.CategoryId != nil && *param.CategoryId > 0 { ads, serr = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, *param.CategoryId, (param.PageNum-1)*param.PageSize, param.PageSize, nil) } else { ads, serr = videoinformationmod.FindManyVideoInformations(ctx, filter, opt) } if serr != nil { return resp, serr } resp.List = ads return resp, nil } // SetVideoRecommend 单/批量设置视频是否推荐 func SetVideoRecommend(ctx context.Context, req *SetRecommendReq) *srvdef.SrvErr { if req == nil || len(req.Ids) == 0 { return srvdef.New(srvdef.ErrParamError, "ids不能为空") } ids, err := normalizeFakeFlowIds(req.Ids) if err != nil { return err } err = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids}, }, bson.M{ "$set": bson.M{ videoinformationmod.FN.VideoInformation.IsRecommend: req.IsRecommend, videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(), }, }) if err != nil { return err } playletcache.ClearHomeCache() return nil } // SetVideoFakeLikes 单/批量随机设置视频假点赞数 func SetVideoFakeLikes(ctx context.Context, req *SetFakeLikesReq) (*SetFakeCountResp, *srvdef.SrvErr) { if req == nil || len(req.Ids) == 0 { return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空") } if req.LikesMin < 0 || req.LikesMax < req.LikesMin { return nil, srvdef.New(srvdef.ErrParamError, "点赞数区间无效") } ids, serr := normalizeFakeFlowIds(req.Ids) if serr != nil { return nil, serr } return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.Likes, req.LikesMin, req.LikesMax) } // SetVideoFakeWatchTimes 单/批量随机设置视频假观看次数 func SetVideoFakeWatchTimes(ctx context.Context, req *SetFakeWatchTimesReq) (*SetFakeCountResp, *srvdef.SrvErr) { if req == nil || len(req.Ids) == 0 { return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空") } if req.WatchTimesMin < 0 || req.WatchTimesMax < req.WatchTimesMin { return nil, srvdef.New(srvdef.ErrParamError, "观看次数区间无效") } ids, err := normalizeFakeFlowIds(req.Ids) if err != nil { return nil, err } return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.WatchTimes, req.WatchTimesMin, req.WatchTimesMax) } // SetVideoHeat 单/批量随机设置视频热度 func SetVideoHeat(ctx context.Context, req *SetVideoHeatReq) (*SetFakeCountResp, *srvdef.SrvErr) { if req == nil || len(req.Ids) == 0 { return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空") } if req.HeatMin < 0 || req.HeatMax < req.HeatMin { return nil, srvdef.New(srvdef.ErrParamError, "热度区间无效") } ids, err := normalizeFakeFlowIds(req.Ids) if err != nil { return nil, err } return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.HeatValue, req.HeatMin, req.HeatMax) } func setVideoFakeField(ctx context.Context, ids []int64, field string, min, max int64) (*SetFakeCountResp, *srvdef.SrvErr) { r := rand.New(rand.NewSource(time.Now().UnixNano())) docs := make([]mongo.WriteModel, 0, len(ids)) for _, id := range ids { docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: id}). SetUpdate(bson.M{"$set": bson.M{field: randomFakeFlowValue(r, min, max)}})) } result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)) if err != nil { return nil, err } if result.MatchedCount == 0 { return nil, srvdef.New(srvdef.ErrNoData, "", "") } playletcache.ClearHomeCache() return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil } func normalizeFakeFlowIds(rawIds []int64) ([]int64, *srvdef.SrvErr) { ids := make([]int64, 0, len(rawIds)) seen := make(map[int64]struct{}, len(rawIds)) for _, id := range rawIds { if id <= 0 { return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id") } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} ids = append(ids, id) } if len(ids) == 0 { return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空") } return ids, nil } const ( defaultFakeFlowMin = int64(1000) defaultFakeFlowMax = int64(10000) ) func randomDefaultFakeFlowPair(r *rand.Rand) (watchTimes, likes int64) { if r == nil { r = rand.New(rand.NewSource(time.Now().UnixNano())) } likes = randomFakeFlowValue(r, defaultFakeFlowMin, defaultFakeFlowMax-1) watchTimes = randomFakeFlowValue(r, likes+1, defaultFakeFlowMax) return watchTimes, likes } func applyDefaultFakeFlow(fake *pubdef.FakeFlow, r *rand.Rand) { if fake == nil { return } fake.WatchTimes, fake.Likes = randomDefaultFakeFlowPair(r) } func defaultFakeFlowSetDoc(r *rand.Rand) bson.M { watchTimes, likes := randomDefaultFakeFlowPair(r) return bson.M{ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: watchTimes, videoinformationmod.FN.VideoInformation.Fake.Likes: likes, } } const fillZeroFakeFlowBatchSize = 500 func zeroFakeFlowFilter() bson.M { return bson.M{ "$or": []bson.M{ { "$expr": bson.M{ "$lt": []interface{}{ bson.M{"$add": []interface{}{"$real.watchTimes", "$fake.watchTimes"}}, defaultFakeFlowMin, }, }, }, { "$expr": bson.M{ "$lt": []interface{}{ bson.M{"$add": []interface{}{"$real.likes", "$fake.likes"}}, defaultFakeFlowMin, }, }, }, }, } } // FillZeroVideoFakeFlow 为播放量或收藏总量小于 1000 的视频随机设置 fake 播放量和收藏数(1000-10000,播放量大于收藏量)。 func FillZeroVideoFakeFlow(ctx context.Context) (*SetFakeCountResp, *srvdef.SrvErr) { filter := zeroFakeFlowFilter() opt := options.Find(). SetProjection(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}). SetBatchSize(fillZeroFakeFlowBatchSize). SetLimit(fillZeroFakeFlowBatchSize) r := rand.New(rand.NewSource(time.Now().UnixNano())) resp := &SetFakeCountResp{} var lastID int64 for { pageFilter := filter if lastID > 0 { pageFilter = bson.M{ "$and": []bson.M{ filter, {videoinformationmod.FN.VideoInformation.Id: bson.M{"$gt": lastID}}, }, } } findOpt := opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}) list, serr := videoinformationmod.FindManyVideoInformations(ctx, pageFilter, findOpt) if serr != nil { return nil, serr } if len(list) == 0 { break } docs := make([]mongo.WriteModel, 0, len(list)) for _, item := range list { if item == nil || item.Id <= 0 { continue } lastID = item.Id resp.MatchedCount++ docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}). SetUpdate(bson.M{"$set": defaultFakeFlowSetDoc(r)})) } if len(docs) == 0 { break } result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)) if err != nil { return nil, err } resp.ModifiedCount += result.ModifiedCount if len(list) < fillZeroFakeFlowBatchSize { break } } if resp.ModifiedCount > 0 { playletcache.ClearHomeCache() } return resp, nil } func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 { if min == max { return min } return min + r.Int63n(max-min+1) } // SyncVideoInfo 同步视频 func SyncVideoInfo(ctx context.Context, req *AddVideoReq) *srvdef.SrvErr { ids := make([]string, 0) for _, id := range req.FileIds { ids = append(ids, id) } if len(ids) == 0 { return nil } queryMediaReq := &proxy.MediaQueryesReq{ Ids: ids, } resp, err := storage.QueryMedias(ctx, queryMediaReq, "") if err != nil || resp.Code != http.StatusOK { if resp != nil { log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err), log.Any("respCode", resp.Code)) } else { log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err)) } return srvdef.New(srvdef.ErrMediaNotExsit, err.Error(), err.Error()) } if len(resp.Data) == 0 { return nil } for _, media := range resp.Data { se := SyncOneVideo(ctx, media) if se != nil { log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(se)) continue } } return nil } func mediaFileDomain(sourceVersion string) string { if sourceVersion == "v1" { return proxy.BucketVideoAddressV1 } return proxy.BucketVideoAddressV2 } func buildMediaRespFromSyncItem(item SyncMediaFileItem) (proxy.MediaResp, error) { oid, err := primitive.ObjectIDFromHex(item.Id) if err != nil { return proxy.MediaResp{}, err } return proxy.MediaResp{ Id: oid, MediaSourceId: oid, MediaType: 2, Title: item.Title, FieldNameFs: item.FieldNameFs, FirstTag: item.FirstTag, SecondTags: item.SecondTags, Keyworld: item.Keyworld, VideoTime: item.VideoTime, CoverImg: item.CoverImg, Publish: item.Publish, Bango: item.Bango, TaskID: item.TaskID, Width: item.Width, Height: item.Height, }, nil } // SyncVideoFromMediaFile 从媒资库直同步到视频列表(不经过切片任务)。 func SyncVideoFromMediaFile(ctx context.Context, req *SyncMediaFileReq) (*SyncMediaFileResp, *srvdef.SrvErr) { result := &SyncMediaFileResp{} if req == nil { return result, nil } minVideoSeconds := req.MinVideoSeconds syncOne := func(id string, media proxy.MediaResp) { if media.Id.IsZero() { result.Failed++ return } if minVideoSeconds > 0 && media.VideoTime > 0 && media.VideoTime <= minVideoSeconds { log.Info("SyncVideoFromMediaFile skip short video", log.Any("id", id), log.Any("videoTime", media.VideoTime)) result.Failed++ return } if len(media.FieldNameFs) == 0 { log.Warn("SyncVideoFromMediaFile empty fieldNameFs", log.Any("id", id)) result.Failed++ return } media.MediaSourceId = media.Id media.MediaType = 2 if req.FirstTagMap != nil { if firstTag, ok := req.FirstTagMap[id]; ok && len(firstTag) > 0 { media.FirstTag = firstTag } } if se := SyncOneVideo(ctx, media); se != nil { log.Warn("SyncVideoFromMediaFile SyncOneVideo failed", log.Any("id", id), log.E(se)) result.Failed++ return } result.Synced++ } if len(req.VideoList) > 0 { for _, item := range req.VideoList { media, err := buildMediaRespFromSyncItem(item) if err != nil { log.Warn("SyncVideoFromMediaFile invalid item", log.Any("id", item.Id), log.E(err)) result.Failed++ continue } syncOne(item.Id, media) } return result, nil } domain := mediaFileDomain(req.SourceVersion) for _, id := range req.FileIds { if len(id) == 0 { continue } resp, err := storage.QueryMedia(ctx, &proxy.MediaQueryeReq{Id: id}, domain) if err != nil || resp == nil || resp.Code != proxy.Success { if resp != nil { log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err), log.Any("respCode", resp.Code)) } else { log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err)) } result.Failed++ continue } media := resp.Data if media.Id.IsZero() { oid, parseErr := primitive.ObjectIDFromHex(id) if parseErr != nil { log.Warn("SyncVideoFromMediaFile invalid id", log.Any("id", id), log.E(parseErr)) result.Failed++ continue } media.Id = oid } syncOne(id, media) } return result, nil } // DelVideoInfo 删除指定APP func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr { info, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{ videoinformationmod.FN.VideoInformation.Id: id, }, options.FindOne().SetProjection(bson.D{ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1}, {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1}, })) if err1 != nil { return err1 } adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id} //删除 count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter) if serr != nil { return serr } if count == 0 { return srvdef.New(srvdef.ErrNoData, "", "") } playletcache.ClearHomeCache() // 删除后检查一下合集数据 collectionInfos, _ := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id, }) if len(collectionInfos) > 0 { collectionIds := make([]int64, 0) for _, collectionInfo := range collectionInfos { collectionIds = append(collectionIds, collectionInfo.CollectionID) } if len(collectionIds) > 0 { _ = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": collectionInfos}, }, bson.M{"$inc": bson.M{ videoinformationmod.FN.VideoInformation.VideoCount: -1, }}) } } _, _ = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id, }) if len(info.Tags) > 0 { oldTagIds := append([]int64{}, info.Tags...) godef.Go(func() { _ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, nil, contenttagmod.ContentTagTypeVideo) }) } return nil } // UpdateVideoInfo 更新指定APP func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate) *srvdef.SrvErr { now := time.Now() info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: id}) if serr != nil { return serr } param.UpdatedAt = &now param.SecondUpdatedAt = &now if param.CreatorId != nil { name := "" if *param.CreatorId == 0 { param.CreatorName = &name } else { // 查询作者 c, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{ videocreatermod.FN.VideoCreaterInfo.Id: *param.CreatorId, }) if serr != nil { return serr } name = c.Name param.CreatorName = &name } } if param.Tags != nil { tagIds, err1 := srv_contenttag.NormalizeContentTagIds(*param.Tags) if err1 != nil { return err1 } if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil { return err1 } param.Tags = &tagIds } if param.CategoryRanks != nil { seen := make(map[int64]bool, len(*param.CategoryRanks)) for _, rank := range *param.CategoryRanks { if rank.Id <= 0 || seen[rank.Id] { return srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "分类ID无效或重复") } seen[rank.Id] = true if _, err := videocategorymod.FindOneVideoCategory(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: rank.Id}); err != nil { if err.Code == srvdef.ErrNoData { return srvdef.New(srvdef.ErrParamError, "视频分类不存在", "视频分类不存在") } return err } } } isChangeActress := checkActressChange(*info, param) isChangeTag := checkTagChange(*info, param) if param.ActressIds != nil && len(*param.ActressIds) > 0 { acts, serr := actress.FindManyActresss(ctx, bson.M{ actress.FN.Actress.Id: bson.M{"$in": *param.ActressIds}, }) if serr != nil { log.Warn("UpdateVideoInfo FindManyActresss failed", log.Any("filter", param.ActressIds), log.E(serr)) return serr } acIds := make([]int64, 0) for _, a := range acts { acIds = append(acIds, a.Id) } param.ActressIds = &acIds } if param.Nodes != nil && len(*param.Nodes) > 0 { nodes := *param.Nodes for i, node := range nodes { if len(node.NodeId) == 0 { node.NodeId = primitive.NewObjectID().Hex() nodes[i] = node } } param.Nodes = &nodes } if param.Cover != nil && len(*param.Cover) > 0 { if len(info.Video.Url) > 0 { info.Video.Cover = *param.Cover param.Video = &info.Video } } if param.Status != nil && *param.Status != info.Status { isChangeActress = true tempActressIds := make([]int64, 0) if param.ActressIds != nil { tempActressIds = *param.ActressIds } go updateActressWorks(info.ActressIds, tempActressIds) } title := info.Title if param.Title != nil { title = *param.Title } keyworld := info.Keyworld if param.Keyworld != nil { keyworld = *param.Keyworld } exists := func(s string) bool { doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter( videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, id)) return doc != nil } slug, changed, slugErr := slugx.PrepareOnUpdate(info.Slug, param.Slug, utils.SlugSourceFromKeywordsOrTitle(keyworld, title), exists) if slugErr != nil { return slugErr } if changed { param.Slug = &slug } else { param.Slug = nil } adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id} param.IndexNow = nil isPublished := param.Status != nil && info.Status != *param.Status && *param.Status == pubdef.InformationStatusVerifyOK if isPublished || (param.IsReIndexNow != nil && *param.IsReIndexNow) { indexNow := int64(0) param.IndexNow = &indexNow } serr = videoinformationmod.UpdateOneVideoInformation(ctx, adFilter, bson.M{"$set": param}) if serr != nil { return serr } playletcache.ClearHomeCache() if isChangeActress { tempActressIds := make([]int64, 0) if param.ActressIds != nil { tempActressIds = *param.ActressIds } go updateActressWorks(info.ActressIds, tempActressIds) } if isChangeTag { tempIds := make([]int64, 0) if param.Tags != nil { tempIds = *param.Tags } oldTagIds := append([]int64{}, info.Tags...) newTagIds := append([]int64{}, tempIds...) godef.Go(func() { _ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, newTagIds, contenttagmod.ContentTagTypeVideo) }) } if param.Status != nil && info.Status != *param.Status { if flag := srv_report.VideoStatusFlag(*param.Status); flag != 0 { mediaCopy := *info mediaCopy.Status = *param.Status godef.Go(func() { srv_report.ReportVideoStatusChangeEvent(&mediaCopy, flag, userId) }) } } newStatus := info.Status if param.Status != nil { newStatus = *param.Status } srv_seopush.NotifyInfoAfterWrite(ctx, srv_seopush.KindVideo, id, info.Status, newStatus) return nil } func checkActressChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool { if updateParam.ActressIds != nil { if len(info.ActressIds) != len(*updateParam.ActressIds) { return true } actressIds := *updateParam.ActressIds for i, v := range info.ActressIds { if v != actressIds[i] { return true } } } return false } func checkTagChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool { if updateParam.Tags != nil { if len(info.Tags) != len(*updateParam.Tags) { return true } tags := *updateParam.Tags for i, v := range info.Tags { if v != tags[i] { return true } } } return false } func updateActressWorks(oldIds []int64, newIds []int64) { defer func() { if err := recover(); err != nil { log.Error("updateActressWorks failed", log.Any("err", err)) } }() mapIds := make(map[int64]struct{}) if oldIds == nil { oldIds = make([]int64, 0) } if newIds == nil { newIds = make([]int64, 0) } for _, id := range newIds { mapIds[id] = struct{}{} } for _, id := range oldIds { mapIds[id] = struct{}{} } ids := make([]int64, 0) for id := range mapIds { ids = append(ids, id) } ctx := context.Background() for _, id := range ids { count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK, videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{id}}}) if err == nil { _ = actress.UpdateOneActress(ctx, bson.M{ actress.FN.Actress.Id: id, }, bson.M{ "$set": bson.M{ actress.FN.Actress.Works: count, }, }) } } } func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr { tagIds, err1 := srv_contenttag.NormalizeContentTagIds(param.Tags) if err1 != nil { return err1 } if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil { return err1 } info := videoinformationmod.VideoInformation{ Id: mongos.Lid.VideoInfo.Get(), Title: param.Title, MediaType: videoinformationmod.MediaType_Collection, AddedTime: time.Now(), Keyworld: param.Keyworld, FirstTag: param.FirstTag, SecondTags: param.SecondTags, CoverImg: param.CoverImg, Desc: param.Desc, Remark: param.Remark, Nodes: make([]videoinformationmod.Node, 0), Status: pubdef.InformationStatusWaiting, Video: videoinformationmod.VideoInfo{}, Tags: tagIds, Real: pubdef.Flow{}, VideoCount: 0, UpdatedAt: time.Now(), CreatedAt: time.Now(), } if err1 := videoinformationmod.InsertOneVideoInformation(ctx, &info); err1 != nil { return err1 } return srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypeVideo) } // AddVideoTags 批量追加视频统一标签 func AddVideoTags(ctx context.Context, req *VideoAddTagsReq) *srvdef.SrvErr { if req == nil || len(req.VideoIds) == 0 || len(req.Tags) == 0 { return srvdef.New(srvdef.ErrParamError, "视频ID和标签ID不能为空", "") } videoIds, err := normalizeFakeFlowIds(req.VideoIds) if err != nil { return err } tagIds, err := srv_contenttag.NormalizeContentTagIds(req.Tags) if err != nil { return err } if len(tagIds) == 0 { return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "") } if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil { return err } list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds}, }, options.Find().SetProjection(bson.D{ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1}, {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1}, })) if err != nil { return err } if len(list) == 0 { return srvdef.New(srvdef.ErrNoData, "", "") } now := time.Now() docs := make([]mongo.WriteModel, 0, len(list)) changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list)) for _, item := range list { if item == nil { continue } newTagIds, changed := appendMissingVideoTagIds(item.Tags, tagIds) if !changed { continue } docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}). SetUpdate(bson.M{"$set": bson.M{ videoinformationmod.FN.VideoInformation.Tags: newTagIds, videoinformationmod.FN.VideoInformation.UpdatedAt: now, }})) changes = append(changes, srv_contenttag.ContentTagBindChange{ OldTagIds: append([]int64{}, item.Tags...), NewTagIds: newTagIds, }) } if len(docs) == 0 { return nil } if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil { return err } playletcache.ClearHomeCache() return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo) } // SetVideoTags 单/批量覆盖设置视频统一标签,并同步新标签表视频绑定数量 func SetVideoTags(ctx context.Context, req *VideoSetTagsReq) *srvdef.SrvErr { if req == nil || len(req.Ids) == 0 { return srvdef.New(srvdef.ErrParamError, "视频ID不能为空", "") } if req.TagIds == nil { return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "") } videoIds, err := normalizeFakeFlowIds(req.Ids) if err != nil { return err } tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds) if err != nil { return err } if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil { return err } list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds}, }, options.Find().SetProjection(bson.D{ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1}, {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1}, })) if err != nil { return err } if len(list) == 0 { return srvdef.New(srvdef.ErrNoData, "", "") } now := time.Now() docs := make([]mongo.WriteModel, 0, len(list)) changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list)) for _, item := range list { if item == nil || sameVideoTagIds(item.Tags, tagIds) { continue } docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}). SetUpdate(bson.M{"$set": bson.M{ videoinformationmod.FN.VideoInformation.Tags: tagIds, videoinformationmod.FN.VideoInformation.UpdatedAt: now, }})) changes = append(changes, srv_contenttag.ContentTagBindChange{ OldTagIds: append([]int64{}, item.Tags...), NewTagIds: append([]int64{}, tagIds...), }) } if len(docs) == 0 { return nil } if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil { return err } playletcache.ClearHomeCache() return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo) } func sameVideoTagIds(current, target []int64) bool { if len(current) != len(target) { return false } for i := range current { if current[i] != target[i] { return false } } return true } func appendMissingVideoTagIds(current, addIds []int64) ([]int64, bool) { result := append([]int64{}, current...) tagMap := make(map[int64]struct{}, len(current)+len(addIds)) for _, tagId := range current { if tagId > 0 { tagMap[tagId] = struct{}{} } } changed := false for _, tagId := range addIds { if _, ok := tagMap[tagId]; ok { continue } tagMap[tagId] = struct{}{} result = append(result, tagId) changed = true } return result, changed }