package vidmod import ( "bytes" "context" "encoding/json" "errors" "fmt" "math" "sort" "strings" "sync" "time" "91porn-server/app/appg" "91porn-server/common" "91porn-server/common/db" "91porn-server/common/localcache" "91porn-server/common/log" "91porn-server/common/pageopt" "91porn-server/common/stderr" "91porn-server/models" "91porn-server/models/commod" "91porn-server/models/v/vidpopmod" "github.com/shopspring/decimal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var mdb *db.MongoDB // 推荐将选比例 var chosonRate = 0.8 const ( ToppingLimit = 5 // ToppingLimit 置顶、力荐、置精数量限制 table = models.VideoInfo vidPopCfgKey = "vidPopCfgKey" allVidWithPop = "allVidWithPop" ) func coll(t *db.MongoTool) *db.MongoTool { if t == nil { return mdb.Coll(table) } return t.Coll(table) } // initIndex 初始化索引 func initIndex() { many := []mongo.IndexModel{ { Keys: bson.D{{Key: "sourceID", Value: 1}}, Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"sourceID": bson.M{"$gt": ""}}), }, // skd 同步数据到es的时候需要按照更新时间筛选 { Keys: bson.D{{"updatedAt", -1}}, }, { Keys: bson.D{{Key: "commentCount", Value: -1}}, }, { Keys: bson.D{{Key: "playCount", Value: -1}}, }, { // 针对视频热度排序查询 Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { // 针对视频播放最多排序查询 Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { // 针对视频点赞最多排序查询 2抖音-推荐排序 Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "isTopping", Value: -1}, {Key: "status", Value: -1}, {Key: "createdAt", Value: -1}}, }, { Keys: bson.D{{Key: "publisherID", Value: 1}, {Key: "status", Value: 1}, {Key: "worksSort", Value: -1}, {Key: "createdAt", Value: -1}}, }, { Keys: bson.D{{Key: "createdAt", Value: 1}}, }, { Keys: bson.D{{Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "coins", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: 1}}, }, // 由于废除了isTopping字段置顶,所以上线后这个索引要删掉 //{ // Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, //}, // 置顶改为有数值的置顶,废除isTopping字段 // 首页最新展示样式模块 { Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, // 首页-最新 { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, // 首页-最多观看 { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 首页-最多观看 { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 首页-热门推荐 { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 首页-最多收藏 { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 黄游-畅销(最多购买) { Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "purchaseCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, }, // 由于废除了isTopping字段置顶,所以上线后这些个索引要删掉 //{ // Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, //}, // 置顶改为有数值的置顶,废除isTopping字段 // 社区-最新/标签新 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, // 社区-推荐/标签最多收藏 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 社区-最热/标签最多观看 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 社区-精选 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 社区-视频/标签-最多观看--按照类型查询 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 标签-最多收藏 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 标签-最新-按照类型查询 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, // /标签-最多收藏-按照类型查询 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // /标签-最多收藏-按照类型查询 { Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { // 热评 Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "status", Value: 1}, {Key: "isTopping", Value: -1}, {Key: "reviewAt", Value: 1}}, }, { // 各类视频热度排序查询 Keys: bson.D{{Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { // 每日短视频推荐分数按 _id 稳定分批扫描 Keys: bson.D{ {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "deleteAt", Value: 1}, {Key: "_id", Value: 1}, }, Options: options.Index().SetName(shortRecommendRefreshIndexName), }, // 折扣专区使用的索引 { Keys: bson.D{{Key: "discountAreaId", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "discountAreaId", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, // 2025--- 新版本 --------------------- { Keys: bson.D{{Key: "publisherID", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, }, { // 排行榜总榜查询 Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}, {Key: "collectCount", Value: -1}}, }, { // 短视频推荐(精选靠前) Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {"chosen", -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, }, { Keys: bson.D{{Key: "showType", Value: 1}}, }, { Keys: bson.D{{Key: "liaoBaTopSort", Value: 1}, {Key: "reviewAt", Value: -1}}, }, { // H.265 等待队列:按状态和入队时间读取待提交任务。 Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265QueuedAt", Value: 1}}, }, { // H.265 云端处理中队列:使用专用时间,避免普通业务更新影响超时判断。 Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265PendingAt", Value: 1}}, }, { // H.265 失败重试:按状态、失败次数和审核时间读取可重试任务。 Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265FailCount", Value: 1}, {Key: "reviewAt", Value: -1}}, }, } if _, err := coll(nil).CreateIndex(many); err != nil { panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) } } type RecommendInteraction string const ( RecommendInteractionLike RecommendInteraction = "like" RecommendInteractionCollect RecommendInteraction = "collect" RecommendInteractionComment RecommendInteraction = "comment" RecommendInteractionShare RecommendInteraction = "share" ) // IncrementRecommendInteraction 累加真实互动推荐值。取消、删除和运营假数据不得调用。 func IncrementRecommendInteraction(id ObjectID, interaction RecommendInteraction) error { return IncrementRecommendInteractionContext(context.Background(), id, interaction) } // IncrementRecommendInteractionContext 累加真实互动推荐值,并继承调用方的取消和超时。 func IncrementRecommendInteractionContext( ctx context.Context, id ObjectID, interaction RecommendInteraction, ) error { field := "" switch interaction { case RecommendInteractionLike: field = "recommendLikeCount" case RecommendInteractionCollect: field = "recommendCollectCount" case RecommendInteractionComment: field = "recommendCommentCount" case RecommendInteractionShare: field = "recommendShareCount" default: return fmt.Errorf("unknown recommend interaction: %s", interaction) } initialized := bson.M{ "_id": id, "status": CheckPass, "newsType": SHORT, "recommendInitialized": true, } collection := coll(mdb.ToolCtx(ctx)) result, err := collection.UpdateOne(initialized, bson.M{"$inc": bson.M{field: 1}}) if err != nil || result.MatchedCount > 0 { return err } // 初始化只会由一个并发请求成功;失败者随后重试普通 $inc,避免丢互动。 initializing := bson.M{ "_id": id, "status": CheckPass, "newsType": SHORT, "recommendInitialized": bson.M{"$ne": true}, } result, err = collection.UpdateOne(initializing, recommendInteractionInitializationPipeline(field)) if err != nil || result.MatchedCount > 0 { return err } _, err = collection.UpdateOne(initialized, bson.M{"$inc": bson.M{field: 1}}) return err } func recommendInteractionInitializationPipeline(incrementField string) mongo.Pipeline { countFields := []struct { recommend string legacy string }{ {recommend: "recommendLikeCount", legacy: "likeCount"}, {recommend: "recommendCollectCount", legacy: "collectCount"}, {recommend: "recommendCommentCount", legacy: "commentCount"}, {recommend: "recommendShareCount", legacy: "shareCount"}, } set := bson.M{"recommendInitialized": true} for _, countField := range countFields { current := bson.M{"$ifNull": bson.A{"$" + countField.recommend, int64(0)}} baseline := bson.M{"$max": bson.A{ current, bson.M{"$max": bson.A{ bson.M{"$ifNull": bson.A{"$" + countField.legacy, int64(0)}}, int64(0), }}, }} if countField.recommend == incrementField { baseline = bson.M{"$add": bson.A{baseline, int64(1)}} } set[countField.recommend] = baseline } return mongo.Pipeline{bson.D{{Key: "$set", Value: set}}} } const ( shortRecommendRefreshIndexName = "idx_short_recommend_scan" recommendScoreBatchSize = 1000 recommendScoreCursorBatchSize = 4000 recommendScoreWorkerCount = 2 recommendScoreCursorCloseTimeout = 5 * time.Second ) // RecommendCandidate 是生成全局队列所需的最小视频快照。 type RecommendCandidate struct { ID ObjectID MID string ReviewAt time.Time RecommendScore int64 } // recommendScoreDocument 仅在单个扫描批次内存活,避免全局队列长期持有互动明细。 type recommendScoreDocument struct { ID ObjectID `bson:"_id"` MID string `bson:"mId"` ReviewAt time.Time `bson:"reviewAt"` LikeCount int `bson:"likeCount"` CollectCount int `bson:"collectCount"` CommentCount int `bson:"commentCount"` ShareCount int `bson:"shareCount"` RecommendLikeCount int64 `bson:"recommendLikeCount"` RecommendCollectCount int64 `bson:"recommendCollectCount"` RecommendCommentCount int64 `bson:"recommendCommentCount"` RecommendShareCount int64 `bson:"recommendShareCount"` RecommendScore *int64 `bson:"recommendScore"` RecommendInitialized bool `bson:"recommendInitialized"` } type recommendScoreStore interface { MaxEligibleID(ctx context.Context) (ObjectID, error) OpenCursor(ctx context.Context, maxID ObjectID, batchSize int32) (recommendScoreCursor, error) BulkWrite(ctx context.Context, writes []mongo.WriteModel) error } type recommendInitializationCounter interface { CountUninitialized(ctx context.Context, maxID ObjectID, limit int64) (int64, error) } type recommendScoreCursor interface { Next(context.Context) bool Decode(interface{}) error Err() error Close(context.Context) error } type recommendScoreScope struct { generatedAt time.Time excludedModuleIDs []string } func newRecommendScoreScope(generatedAt time.Time, excludedModuleIDs []string) recommendScoreScope { seen := make(map[string]struct{}, len(excludedModuleIDs)) normalized := make([]string, 0, len(excludedModuleIDs)) for _, moduleID := range excludedModuleIDs { moduleID = strings.TrimSpace(moduleID) if moduleID == "" { continue } if _, ok := seen[moduleID]; ok { continue } seen[moduleID] = struct{}{} normalized = append(normalized, moduleID) } return recommendScoreScope{ generatedAt: generatedAt, excludedModuleIDs: normalized, } } func (s recommendScoreScope) filter() bson.M { filter := bson.M{ "status": CheckPass, "newsType": SHORT, "deleteAt": nil, "recoWeight": bson.M{"$ne": -1}, // 审核通过时间晚于本次构建快照的视频不能进入新视频池或高分池。 "reviewAt": bson.M{"$lte": s.generatedAt}, } if len(s.excludedModuleIDs) > 0 { // mId $nin 的选择性通常较低,不为它单独增加索引;Mongo 继续使用 // newsType/status/deleteAt/_id 稳定扫描索引,并在服务端尽早做残余过滤。 filter["mId"] = bson.M{"$nin": append([]string(nil), s.excludedModuleIDs...)} } return filter } type mongoRecommendScoreStore struct { scope recommendScoreScope } func (s mongoRecommendScoreStore) MaxEligibleID(ctx context.Context) (ObjectID, error) { var latest struct { ID ObjectID `bson:"_id"` } filter, opts := recommendScoreMaxEligibleIDQuery(s.scope) err := coll(mdb.ToolCtx(ctx)).FindOne(&latest, filter, opts) return recommendScoreMaxEligibleIDResult(latest.ID, err) } func recommendScoreMaxEligibleIDResult(id ObjectID, err error) (ObjectID, error) { if errors.Is(err, mongo.ErrNoDocuments) { return primitive.NilObjectID, nil } if err != nil { return primitive.NilObjectID, err } return id, nil } func recommendScoreMaxEligibleIDQuery( scope recommendScoreScope, ) (bson.M, *options.FindOneOptions) { opts := options.FindOne(). SetProjection(bson.M{"_id": 1}). SetSort(bson.D{{Key: "_id", Value: -1}}). SetHint(shortRecommendRefreshIndexName) return scope.filter(), opts } func recommendScoreCursorQuery( maxID ObjectID, batchSize int32, scope recommendScoreScope, ) (bson.M, *options.FindOptions) { filter := scope.filter() filter["_id"] = bson.M{"$lte": maxID} opts := options.Find(). SetProjection(bson.M{ "_id": 1, "mId": 1, "reviewAt": 1, "likeCount": 1, "collectCount": 1, "commentCount": 1, "shareCount": 1, "recommendLikeCount": 1, "recommendCollectCount": 1, "recommendCommentCount": 1, "recommendShareCount": 1, "recommendScore": 1, "recommendInitialized": 1, }). SetSort(bson.D{{Key: "_id", Value: 1}}). SetBatchSize(batchSize). SetHint(shortRecommendRefreshIndexName) return filter, opts } func (s mongoRecommendScoreStore) OpenCursor( ctx context.Context, maxID ObjectID, batchSize int32, ) (recommendScoreCursor, error) { filter, opts := recommendScoreCursorQuery(maxID, batchSize, s.scope) return coll(mdb.ToolCtx(ctx)).FindCursor(filter, opts) } func (mongoRecommendScoreStore) BulkWrite(ctx context.Context, writes []mongo.WriteModel) error { _, err := coll(mdb.ToolCtx(ctx)).Bulk(writes, options.BulkWrite().SetOrdered(false)) return err } func (s mongoRecommendScoreStore) CountUninitialized( ctx context.Context, maxID ObjectID, limit int64, ) (int64, error) { if limit <= 0 { return 0, nil } filter, opts := recommendInitializationCountQuery(maxID, limit, s.scope) return coll(mdb.ToolCtx(ctx)).Count(filter, opts) } func recommendInitializationCountQuery( maxID ObjectID, limit int64, scope recommendScoreScope, ) (bson.M, *options.CountOptions) { filter := scope.filter() filter["_id"] = bson.M{"$lte": maxID} filter["recommendInitialized"] = bson.M{"$ne": true} opts := options.Count(). SetHint(shortRecommendRefreshIndexName). SetLimit(limit) return filter, opts } func nonNegativeInt(v int) int64 { if v < 0 { return 0 } return int64(v) } func maxInt64(a, b int64) int64 { if a > b { return a } return b } func CalculateRecommendScore(like, collect, comment, share int64) int64 { return like + collect*2 + comment*3 + share*5 } func compareObjectID(a, b ObjectID) int { return bytes.Compare(a[:], b[:]) } func prepareRecommendScoreBatch( documents []recommendScoreDocument, now time.Time, ) ([]RecommendCandidate, []mongo.WriteModel) { candidates := make([]RecommendCandidate, 0, len(documents)) var writes []mongo.WriteModel var updatePipeline mongo.Pipeline for i := range documents { document := &documents[i] like := document.RecommendLikeCount collect := document.RecommendCollectCount comment := document.RecommendCommentCount share := document.RecommendShareCount // 传统真实计数是增量链路的每日补偿源;取max既能补偿瞬时漏写, // 又保证取消点赞/收藏或删除评论时推荐累计分不回退。 like = maxInt64(like, nonNegativeInt(document.LikeCount)) collect = maxInt64(collect, nonNegativeInt(document.CollectCount)) comment = maxInt64(comment, nonNegativeInt(document.CommentCount)) share = maxInt64(share, nonNegativeInt(document.ShareCount)) score := CalculateRecommendScore(like, collect, comment, share) candidates = append(candidates, RecommendCandidate{ ID: document.ID, MID: document.MID, ReviewAt: document.ReviewAt, RecommendScore: score, }) if document.RecommendInitialized && document.RecommendScore != nil && *document.RecommendScore == score { continue } if updatePipeline == nil { updatePipeline = recommendScoreUpdatePipeline(now) } writes = append(writes, mongo.NewUpdateOneModel(). SetFilter(bson.M{"_id": document.ID}). SetUpdate(updatePipeline)) } return candidates, writes } func recommendScoreUpdatePipeline(now time.Time) mongo.Pipeline { counts := []struct { recommend string legacy string }{ {recommend: "recommendLikeCount", legacy: "likeCount"}, {recommend: "recommendCollectCount", legacy: "collectCount"}, {recommend: "recommendCommentCount", legacy: "commentCount"}, {recommend: "recommendShareCount", legacy: "shareCount"}, } initializeSet := bson.M{"recommendInitialized": true} for _, count := range counts { current := bson.M{"$ifNull": bson.A{"$" + count.recommend, int64(0)}} baseline := bson.M{"$max": bson.A{ current, bson.M{"$max": bson.A{ bson.M{"$ifNull": bson.A{"$" + count.legacy, int64(0)}}, int64(0), }}, }} initializeSet[count.recommend] = baseline } score := bson.M{"$add": bson.A{ "$recommendLikeCount", bson.M{"$multiply": bson.A{"$recommendCollectCount", int64(2)}}, bson.M{"$multiply": bson.A{"$recommendCommentCount", int64(3)}}, bson.M{"$multiply": bson.A{"$recommendShareCount", int64(5)}}, }} return mongo.Pipeline{ bson.D{{Key: "$set", Value: initializeSet}}, bson.D{{Key: "$set", Value: bson.M{ "recommendScore": score, "recommendScoreAt": now, }}}, } } func streamRecommendScoreBatches( ctx context.Context, maxID ObjectID, batchSize int, cursorBatchSize int32, store recommendScoreStore, yield func([]recommendScoreDocument) error, ) (err error) { cursor, err := store.OpenCursor(ctx, maxID, cursorBatchSize) if err != nil { return err } if cursor == nil { return errors.New("recommend score cursor is nil") } defer func() { closeCtx, cancel := context.WithTimeout(context.Background(), recommendScoreCursorCloseTimeout) defer cancel() if closeErr := cursor.Close(closeCtx); err == nil && closeErr != nil { err = closeErr } }() batch := make([]recommendScoreDocument, 0, batchSize) previousID := primitive.NilObjectID for cursor.Next(ctx) { var document recommendScoreDocument if err := cursor.Decode(&document); err != nil { return err } if document.ID.IsZero() || compareObjectID(document.ID, previousID) <= 0 || compareObjectID(document.ID, maxID) > 0 { return fmt.Errorf("recommend score cursor returned an invalid _id sequence") } previousID = document.ID batch = append(batch, document) if len(batch) < batchSize { continue } if err := yield(batch); err != nil { return err } // Worker 异步持有已提交批次,必须换一块底层数组,不能 batch[:0] 复用。 batch = make([]recommendScoreDocument, 0, batchSize) } if err := cursor.Err(); err != nil { return err } if len(batch) > 0 { if err := yield(batch); err != nil { return err } } return nil } // refreshRecommendScores 固定扫描高水位,并使用有界 worker 分批刷新推荐分。 func refreshRecommendScores( ctx context.Context, now time.Time, batchSize int, cursorBatchSize int32, workerCount int, store recommendScoreStore, initializationLimits ...int64, ) ([]RecommendCandidate, error) { startedAt := time.Now() if batchSize <= 0 { return nil, fmt.Errorf("recommend score batch size must be positive") } if cursorBatchSize <= 0 { return nil, fmt.Errorf("recommend score cursor batch size must be positive") } if workerCount <= 0 { return nil, fmt.Errorf("recommend score worker count must be positive") } maxID, err := store.MaxEligibleID(ctx) if err != nil { return nil, err } if maxID.IsZero() { return []RecommendCandidate{}, nil } initializationLimit := int64(0) if len(initializationLimits) > 0 { initializationLimit = initializationLimits[0] } if initializationLimit > 0 { counter, ok := store.(recommendInitializationCounter) if !ok { return nil, fmt.Errorf("recommend score store does not support initialization guard") } count, countErr := counter.CountUninitialized(ctx, maxID, initializationLimit+1) if countErr != nil { return nil, countErr } if count > initializationLimit { return nil, RecommendInitializationLimitError{ ObservedAtLeast: count, Limit: initializationLimit, } } } workCtx, cancel := context.WithCancel(ctx) defer cancel() jobs := make(chan []recommendScoreDocument, workerCount) candidates := make([]RecommendCandidate, 0, batchSize*workerCount) scoreWriteCount := 0 bulkWriteCount := 0 var candidatesMu sync.Mutex var firstErr error var errOnce sync.Once fail := func(err error) { if err == nil { return } errOnce.Do(func() { firstErr = err cancel() }) } var workers sync.WaitGroup workers.Add(workerCount) for i := 0; i < workerCount; i++ { go func() { defer workers.Done() for { select { case <-workCtx.Done(): return case batch, ok := <-jobs: if !ok { return } batchCandidates, writes := prepareRecommendScoreBatch(batch, now) if len(writes) > 0 { if err := store.BulkWrite(workCtx, writes); err != nil { fail(err) return } } candidatesMu.Lock() candidates = append(candidates, batchCandidates...) scoreWriteCount += len(writes) if len(writes) > 0 { bulkWriteCount++ } candidatesMu.Unlock() } } }() } var producer sync.WaitGroup producer.Add(1) go func() { defer producer.Done() defer close(jobs) err := streamRecommendScoreBatches( workCtx, maxID, batchSize, cursorBatchSize, store, func(batch []recommendScoreDocument) error { select { case jobs <- batch: return nil case <-workCtx.Done(): return workCtx.Err() } }, ) if err != nil { fail(err) } }() producer.Wait() workers.Wait() if firstErr != nil { return nil, firstErr } if err := ctx.Err(); err != nil { return nil, err } log.Info("short recommend scores refreshed", log.Any("candidateCount", len(candidates)), log.Any("scoreWriteCount", scoreWriteCount), log.Any("bulkWriteCount", bulkWriteCount), log.Any("batchSize", batchSize), log.Any("cursorBatchSize", cursorBatchSize), log.Any("workerCount", workerCount), log.Any("durationMs", time.Since(startedAt).Milliseconds())) return candidates, nil } // RefreshRecommendScores 全量计算短视频推荐分,仅回写首次初始化或分数变化的视频。 func RefreshRecommendScores(ctx context.Context, now time.Time) ([]RecommendCandidate, error) { scope := newRecommendScoreScope(now, nil) return refreshRecommendScores( ctx, now, recommendScoreBatchSize, recommendScoreCursorBatchSize, recommendScoreWorkerCount, mongoRecommendScoreStore{scope: scope}, ) } // RecommendInitializationLimitError 表示首次推荐字段初始化量超过发布保护阈值。 // 该错误在任何BulkWrite之前返回,避免生产首发产生不可控的海量写入。 type RecommendInitializationLimitError struct { ObservedAtLeast int64 Limit int64 } func (e RecommendInitializationLimitError) Error() string { return fmt.Sprintf( "short recommend initialization requires at least %d writes, limit is %d", e.ObservedAtLeast, e.Limit, ) } // RefreshRecommendScoresWithInitializationLimit 在刷新前限制首次初始化写入量。 // excludedModuleIDs 会进入初始化计数与扫描共用的Mongo过滤器,确保被排除模块 // 不占用保护额度、不生成候选,也不会触发推荐分BulkWrite。 // limit<=0时关闭保护,行为与RefreshRecommendScores一致。 func RefreshRecommendScoresWithInitializationLimit( ctx context.Context, now time.Time, limit int64, excludedModuleIDs ...string, ) ([]RecommendCandidate, error) { scope := newRecommendScoreScope(now, excludedModuleIDs) return refreshRecommendScores( ctx, now, recommendScoreBatchSize, recommendScoreCursorBatchSize, recommendScoreWorkerCount, mongoRecommendScoreStore{scope: scope}, limit, ) } // GetTotalCnt 获取标签视频总数 func GetTotalCnt(cond bson.M) (int64, error) { total, err := coll(nil).Count(cond) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getTotalCnt", table, "Count", err), log.Any("cond", cond), ) return 0, err } return total, nil } // getPublishedVideoList 条件获取视频列表 func getPublishedVideoList(page, size uint64, cond bson.M, sort bson.D) ([]*VideoModel, int, int64, bool, error) { hasNext := false cond["status"] = 1 opts := options.FindOptions{} if sort != nil { opts.SetSort(sort) } opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size + 1)) var back []*VideoModel if err := coll(nil).Find(&back, cond, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err), log.Any("page", page), log.Any("size", size), log.Any("cond", cond), log.Any("sort", sort), ) return nil, 0, 0, false, err } if uint64(len(back)) > size { hasNext = true back = back[:size] } return back, 1, 1, hasNext, nil } // GetVideoListByCond 条件获取视频列表 func GetVideoListByCond(filter primitive.M, opts ...*options.FindOptions) ([]*VideoModel, error) { var out []*VideoModel if err := coll(nil).Find(&out, filter, opts...); err != nil { log.Error(fmt.Sprintf("[METHOD-QueryAllDocument]==> Model %s Find fail error:%+v:", table, err), log.Any("filter", filter), ) return nil, err } return out, nil } // getPublishedVideoList 条件获取视频列表,供推荐使用 func getPublishedVideoList4Reco(size uint64, cond bson.M, sort bson.D) ([]*RecoVideoModel, error) { var back []*RecoVideoModel if size == 0 { return back, nil } cond["status"] = 1 cond["playTime"] = bson.M{"$lt": 600} opts := options.FindOptions{} if sort != nil { opts.SetSort(sort) } opts.SetProjection(bson.M{"_id": 1, "createdAt": 1, "chosenDate": 1}) opts.SetLimit(int64(size)) if err := coll(nil).Find(&back, cond, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList4Reco", table, "Find", err), log.Any("size", size), log.Any("cond", cond), log.Any("sort", sort), ) return nil, err } return back, nil } // getDistinctVideoID 获取推荐的视频id,发布者去重 func getDistinctVideoID(page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, time.Time, error) { var maxChosenLine time.Time var maxCreateLine time.Time cond["recoWeight"] = bson.M{"$gte": 0} videoInfos, err := getPublishedVideoList4Reco(size, cond, sort) if err != nil { return []primitive.ObjectID{}, maxChosenLine, maxCreateLine, err } videoInfosLen := len(videoInfos) if videoInfosLen == 0 { return []primitive.ObjectID{}, maxChosenLine, maxCreateLine, nil } vids := make([]primitive.ObjectID, 0, videoInfosLen) for _, i := range videoInfos { if i == nil { continue } if i.ChosenDate.After(maxChosenLine) { maxChosenLine = i.ChosenDate } if i.CreatedAt.After(maxCreateLine) { maxCreateLine = i.CreatedAt } vids = append(vids, i.ID) } return vids, maxChosenLine, maxCreateLine, nil } func getChosenVideoID(line time.Time, page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, error) { cond["chosenDate"] = bson.M{"$gt": line} cond["chosen"] = true ids, chonseLine, _, err := getDistinctVideoID(page, size, cond, sort) return ids, chonseLine, err } func getUnChosenVideoID(line time.Time, page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, error) { cond["createdAt"] = bson.M{"$gt": line} cond["chosen"] = false ids, _, createLine, err := getDistinctVideoID(page, size, cond, sort) return ids, createLine, err } func getRecommendVideoIDs(chosenLine time.Time, unpopLine time.Time, cond bson.M, page, size uint64, sort bson.D) ([]primitive.ObjectID, time.Time, time.Time, error) { chosenSize := uint64(float64(size) * chosonRate) if chosenSize == 0 { chosenSize = 1 } var maxChoseLine time.Time var maxCreateLine time.Time idsChosen, maxChoseLine, err := getChosenVideoID(chosenLine, page, chosenSize, cond, sort) if err != nil { return nil, maxChoseLine, maxCreateLine, err } unChosenSize := size - uint64(len(idsChosen)) if unChosenSize == 0 { return idsChosen, maxChoseLine, maxCreateLine, nil } delete(cond, "chosenDate") idsUnchosen, maxCreateLine, err := getUnChosenVideoID(unpopLine, page, unChosenSize, cond, sort) if err != nil { return nil, maxChoseLine, maxCreateLine, err } idsChosen = append(idsChosen, idsUnchosen...) return idsChosen, maxChoseLine, maxCreateLine, nil } // getVideoList 条件获取视频列表 func getVideoList(page, size uint64, cond bson.M, s bson.D) ([]*VideoModel, int64, bool, error) { total, err := coll(nil).Count(cond) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Count", err), log.Any("page", page), log.Any("size", size), log.Any("cond", cond), log.Any("sort", s), ) return nil, 0, false, err } var ( back []*VideoModel opts = options.Find().SetSkip(int64(page-1) * int64(size)).SetLimit(int64(size)) ) if s != nil { opts.SetSort(s) } if err = coll(nil).Find(&back, cond, opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("page", page), log.Any("size", size), log.Any("cond", cond), log.Any("sort", s), ) return nil, 0, false, err } if uint64(len(back)) > size { return back[:size], total, true, nil } return back, total, false, nil } func GetHotList(skip, limit int64, cond bson.M) (back []*VideoModel, total int64, hasNext bool, err error) { // 获取总条数 total, err = coll(nil).Count(cond) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("skip", skip), log.Any("limit", limit), log.Any("cond", cond), ) return } if total == 0 { return } res := make([]*VideoModel, 0) if err = coll(nil).Find(&res, cond); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("skip", skip), log.Any("limit", limit), log.Any("cond", cond), ) return } cfg, err := getVideoPopularityConfig() if err != nil { return } for _, v := range res { v.Hot = calcHot(v, cfg) } sort.Slice(res, func(i, j int) bool { return res[i].Hot > res[j].Hot }) if len(res) <= int(skip) { hasNext = false return } if len(res) <= int(skip+limit) { back = res[skip:] } else { back = res[skip : skip+limit] hasNext = true } return } // 主动设置缓存 func SetAllVidWithPopCache() { res := make([]*VideoModel, 0) filter := bson.M{ "newsType": SP, "status": 1, } if err := coll(nil).Find(&res, filter); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "setAllVidWithPopCache", table, "Find", err), log.Any("cond", filter), ) return } cfg, err := getVideoPopularityConfig() if err != nil { return } for _, v := range res { v.Hot = calcHot(v, cfg) } sort.Slice(res, func(i, j int) bool { return res[i].Hot > res[j].Hot }) localcache.C.Set(allVidWithPop, res, 30*time.Minute) } func GetHotListFromCache(skip, limit int64, listType ListType, tag string, paymentEnum PaymentEnum) (back []*VideoModel, hasNext bool, err error) { res := make([]*VideoModel, 0) if cfg, ok := localcache.C.Get(allVidWithPop); ok { res = cfg.([]*VideoModel) } else { filter := bson.M{ "newsType": SP, "status": 1, } if err = coll(nil).Find(&res, filter); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("skip", skip), log.Any("limit", limit), log.Any("cond", filter), ) return } cfg, err := getVideoPopularityConfig() if err != nil { return nil, false, err } for _, v := range res { v.Hot = calcHot(v, cfg) } sort.Slice(res, func(i, j int) bool { return res[i].Hot > res[j].Hot }) localcache.C.Set(allVidWithPop, res, 30*time.Minute) } if listType != Video || tag != "" || paymentEnum != PaymentDefault { outIndexSli := make([]int, 0) for i, v := range res { if listType == LongVideo && v.PlayTime < 600 { outIndexSli = append([]int{i}, outIndexSli...) continue } if listType == ShortVideo && v.PlayTime >= 600 { outIndexSli = append([]int{i}, outIndexSli...) continue } if tag != "" { hasTag := false for _, t := range v.Tags { if t.Hex() == tag { hasTag = true break } } if !hasTag { outIndexSli = append([]int{i}, outIndexSli...) continue } } if paymentEnum == PaymentVIP && v.Coins > 0 { outIndexSli = append([]int{i}, outIndexSli...) continue } if paymentEnum == PaymentGold && v.Coins == 0 { outIndexSli = append([]int{i}, outIndexSli...) continue } } if len(outIndexSli) > 0 { for oi := range outIndexSli { res = append(res[:outIndexSli[oi]], res[outIndexSli[oi]+1:]...) } } } if len(res) <= int(skip) { hasNext = false return } if len(res) <= int(skip+limit) { back = res[skip:] } else { back = res[skip : skip+limit] hasNext = true } return } func calcHot(video *VideoModel, cfg *vidpopmod.VideoPopularityConfig) float64 { if video.Status == CheckPass || video.Status == Free { qualityScore := float64(video.PlayCount + cfg.EffectivePlayCountMultiplier*video.EffectivePlayCount + cfg.LikeCountMultiplier*video.LikeCount) hot := (qualityScore + float64(cfg.InitialPopularity)) / math.Pow(1+time.Since(video.ReviewAt).Hours(), cfg.ReviewTimePower) return hot } return 0 } func getVideoPopularityConfig() (*vidpopmod.VideoPopularityConfig, error) { cfg, ok := localcache.C.Get(vidPopCfgKey) if ok { return cfg.(*vidpopmod.VideoPopularityConfig), nil } res, err := vidpopmod.FindOne() if err != nil { return nil, err } localcache.C.Set(vidPopCfgKey, &res, 15*time.Minute) return &res, nil } // 获取作品列表 func getWorksList(skip, limit int64, cond bson.M, sort bson.D) (back []*VideoModel, total int64, hasNext bool, err error) { opts := options.FindOptions{} if sort != nil { opts.SetSort(sort) } opts.SetSkip(skip).SetLimit(limit + 1) // 获取总条数 total, err = coll(nil).Count(cond) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("skip", skip), log.Any("limit", limit), log.Any("cond", cond), log.Any("sort", sort), ) return } if total == 0 { return } if err = coll(nil).Find(&back, cond, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("skip", skip), log.Any("limit", limit), log.Any("cond", cond), log.Any("sort", sort), ) return } if len(back) > int(limit) { hasNext = true back = back[:limit] } return } // ModifyMany 批量修改视屏信息 func ModifyMany(ids []primitive.ObjectID, doc SetDoc) (int64, error) { if len(ids) == 0 { return 0, nil } filter := M{ "_id": bson.M{"$in": ids}, } docM, err := common.ToBsonM(doc) if err != nil { return 0, err } update := M{ "$set": docM, } ret, err := coll(nil).UpdateMany(filter, update) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveVideoFromMy", table, "UpdateMany", err), log.Any("updateDoc", doc), ) return 0, err } return ret.ModifiedCount, nil } // CountByCreatedTime CountByCreatedTime func CountByCreatedTime(start time.Time, end time.Time) (int64, error) { count, err := coll(nil).Count(bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByCreatedTime", table, "Count", err)) return count, err } return count, nil } // CountTotalLikeByUID 查询用户被赞总数 func CountTotalLikeByUID(uid uint64) (int64, error) { pipeline := []bson.M{ {"$match": bson.M{"publisherID": uid}}, {"$group": bson.M{"_id": nil, "total": bson.M{"$sum": "$likeCount"}}}, } type res struct { Total int64 `json:"total" bson:"total"` } resA := make([]*res, 0) if err := coll(nil).Aggregate(&resA, pipeline); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountTotalLikeByUID", table, "Aggregate", err), log.Any("uid", uid), ) return 0, err } if len(resA) > 0 { return resA[0].Total, nil } return 0, nil } // 获取广告帖子 func GetAdVideoIdsList(size uint64) []string { cond := bson.M{"status": CheckPass, "newsType": bson.M{"$in": []string{AD_COVER, AD_SP}}, "recoWeight": bson.M{"$gte": 0}} sort := bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}} videoInfos, err := getPublishedVideoList4Reco(size, cond, sort) if err != nil { return []string{} } videoInfosLen := len(videoInfos) if videoInfosLen == 0 { return []string{} } ids := make([]string, videoInfosLen) for i := range videoInfos { ids[i] = videoInfos[i].ID.Hex() } return ids } // GetVideoListByPublishers 通过发布者获取视频列表 func GetVideoListByPublishers(uids []uint64, page, size uint64) ([]*VideoModel, int, bool, error) { if len(uids) == 0 { return []*VideoModel{}, 0, false, nil } cond := bson.M{"publisherID": bson.M{"$in": uids}} // 下层函数已经设置过滤status sort := bson.D{{Key: "reviewAt", Value: -1}} infos, totalPages, _, hasNext, err := getPublishedVideoList(page, size, cond, sort) return infos, totalPages, hasNext, err } // GetVideoListRecommend 获取推荐视频列表 func GetVideoListRecommend(chosenLine time.Time, unpopLine time.Time, newsType, quality, direction string, page, size uint64) ([]*VideoModel, []primitive.ObjectID, int, time.Time, time.Time, error) { cond := bson.M{"newsType": newsType, "coins": 0} if quality != "" { cond["quality"] = quality } if direction != "" { cond["direction"] = direction } sort := bson.D{{Key: "createdAt", Value: 1}} ids, maxChoseLine, maxCreateLine, err := getRecommendVideoIDs(chosenLine, unpopLine, cond, page, size, sort) if err != nil { return nil, nil, 0, maxChoseLine, maxCreateLine, err } newIDs := common.Deduplication(ids) infos, err := GetVideoListByIDs(newIDs) if err != nil { return nil, nil, 0, maxChoseLine, maxCreateLine, err } return infos, newIDs, 1, maxChoseLine, maxCreateLine, nil } func FindOneByUid(cond bson.M) (vidInfo *VideoModel, err error) { sort := D{ {"reviewAt", -1}, } opt := (&options.FindOneOptions{}).SetSort(sort) err = coll(nil).FindOne(&vidInfo, cond, opt) return } // GetUnpopularVideos 获取冷门视频列表 func GetUnpopularVideos(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { cond := bson.M{"createdAt": bson.M{"$gt": line}, "coins": 0, "newsType": newsType} if quality != "" { cond["quality"] = quality } if direction != "" { cond["direction"] = direction } sort := bson.D{{Key: "createdAt", Value: 1}} ids, maxLine, err := getUnChosenVideoID(line, page, size, cond, sort) if err != nil { return nil, maxLine, err } if len(ids) == 0 { //如果没有视频拉取了,重置时间游标 var newRound time.Time ids, maxLine, err = getUnChosenVideoID(newRound, page, size, cond, sort) } arr := common.ObjectIDs2String(ids) return arr, maxLine, err } // GetChargeVideos 获取收费视频 func GetChargeVideos(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { cond := bson.M{"coins": bson.M{"$gt": 0}, "newsType": newsType} if quality != "" { cond["quality"] = quality } if direction != "" { cond["direction"] = direction } sort := bson.D{{Key: "createdAt", Value: 1}} ids, maxLine, err := getUnChosenVideoID(line, page, size, cond, sort) if err != nil { return nil, maxLine, err } if len(ids) == 0 { //如果没有视频拉取了,重置时间游标 var newRound time.Time ids, maxLine, err = getUnChosenVideoID(newRound, page, size, cond, sort) } arr := common.ObjectIDs2String(ids) return arr, maxLine, err } // GetLatestUploadForRecommend 获取最新上传视频列表 func GetLatestUploadForRecommend(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { sort := bson.D{{Key: "createdAt", Value: 1}} filter := bson.M{"createdAt": bson.M{"$gt": line}, "coins": 0, "newsType": newsType} if quality != "" { filter["quality"] = quality } if direction != "" { filter["direction"] = direction } oids, _, maxLine, err := getDistinctVideoID(page, size, filter, sort) ids := common.ObjectIDs2String(oids) return ids, maxLine, err } // GetBloggerVideos 获取原创博主视频 func GetBloggerVideos(bloggerUID uint64, skip, limit int64) (videos []*VideoModel, hasNext bool, err error) { opts := options.Find().SetSkip(skip).SetLimit(limit + 1).SetSort(bson.D{{Key: "createdAt", Value: -1}}) cond := bson.M{"publisherID": bloggerUID, "status": 1, "newsType": SP} if err = coll(nil).Find(&videos, cond, opts); err != nil { return } if len(videos) == int(limit)+1 { hasNext = true videos = videos[:limit] } return } func GetVideoListByIDsSortByPlayCnt(vids []ObjectID, limit int64) (videos []*VideoModel, err error) { cond := M{"_id": bson.M{"$in": vids}} sort := D{{Key: "playCount", Value: -1}} opts := options.FindOptions{} opts.SetSort(sort).SetLimit(limit) err = coll(nil).Find(&videos, cond, &opts) return } // GetVideoListByIDs 通过视频id获取视频列表 func GetVideoListByIDs(vids []ObjectID) ([]*VideoModel, error) { if vids == nil { vids = []ObjectID{} } cond := M{"_id": bson.M{"$in": vids}} infos, _, _, _, err := getPublishedVideoList(1, uint64(len(vids)), cond, nil) return infos, err } // GetVideoListByIDs 通过视频id获取视频列表 func GetVideoListByIDsAndPlayTime(vids []ObjectID, page, size uint64, playTimeType int) ([]*VideoModel, error) { if vids == nil { vids = []ObjectID{} } cond := M{"_id": bson.M{"$in": vids}} switch playTimeType { case 1: cond["playTime"] = bson.M{"$gte": 60 * 10} case 2: cond["playTime"] = bson.M{"$lt": 60 * 10} } infos, _, _, _, err := getPublishedVideoList(page, size+1, cond, nil) return infos, err } // GetVideoByID 根据id获取一条视频记录 func GetVideoByID(id primitive.ObjectID) (videoInfo *VideoModel, err error) { err = coll(nil).FindOne(&videoInfo, bson.M{"_id": id}) return } func GetVideoListByIDsNew(vids []ObjectID) ([]*VideoModel, bool, error) { if vids == nil { vids = []ObjectID{} } cond := M{"_id": bson.M{"$in": vids}} sort := bson.D{{Key: "reviewAt", Value: -1}, {Key: "createdAt", Value: -1}} infos, _, _, hasNext, err := getPublishedVideoList(1, uint64(len(vids)), cond, sort) return infos, hasNext, err } // GetVideoListByIDs 通过视频id获取视频列表 底层不判断视频状态, 由上次业务保证ID的正确性 func GetVideoListByIDsNoStatus(vids []ObjectID) ([]*VideoModel, error) { if vids == nil { vids = []ObjectID{} } cond := M{"_id": bson.M{"$in": vids}} sort := D{{Key: "reviewAt", Value: -1}} infos, _, _, err := getVideoList(1, uint64(len(vids)), cond, sort) return infos, err } // GetVideoListByIDsUnPublish 通过视频id获取状态不为审核通过的视频 func GetVideoListByIDsUnPublish(vids []ObjectID) (back []*RecoVideoModel, err error) { opts := options.Find() opts.SetProjection(bson.M{"_id": 1}) cond := M{"_id": bson.M{"$in": vids}, "status": bson.M{"$ne": CheckPass}} if err = coll(nil).Find(&back, cond, opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoListByIDsUnPublish", table, "Find", err)) } return } // VideoMap VideoMap func VideoMap(vids []ObjectID) (map[ObjectID]*VideoModel, error) { filter := M{ "_id": bson.M{"$in": vids}, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } list := make([]VideoModel, 0, len(vids)) if err := coll(nil).Find(&list, filter); err != nil { return nil, err } m := make(map[ObjectID]*VideoModel) for _, v := range list { vid := v m[vid.ID] = &vid } return m, nil } // SearchVideoMap 搜索 func SearchVideoMap(vids []ObjectID) (map[ObjectID]*VideoModel, []VideoModel, error) { filter := M{ "_id": bson.M{"$in": vids}, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } list := make([]VideoModel, 0, len(vids)) if err := coll(nil).Find(&list, filter); err != nil { return nil, nil, err } m := make(map[ObjectID]*VideoModel) for _, v := range list { vid := v m[vid.ID] = &vid } return m, list, nil } // GetHisWorkList 获取作品列表 func GetHisWorkList(uid uint64, sikp, limit int64, status *int, sortType string, playTimeType int) ([]*VideoModel, int64, bool, error) { cond := bson.M{"publisherID": uid} switch playTimeType { case 1: cond["newsType"] = SP case 2: cond["newsType"] = SHORT case 3: cond["newsType"] = COVER case 4: cond["newsType"] = PIC default: cond["newsType"] = bson.M{"$in": []string{SP, SHORT, COVER, PIC}} } if status != nil { cond["status"] = status } var sort bson.D switch sortType { case "hot": return GetHotList(sikp, limit, cond) case "watch": sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} case "like": sort = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} case "new", "": sort = bson.D{{Key: "reviewAt", Value: -1}} default: //兼容旧版,传入参数为其他值 sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} } // infos, _, total, hasNext, err := getPublishedVideoList(page, size, cond, sort) return getWorksList(sikp, limit, cond, sort) } // GetMyWorkList 获取我自己作品列表 func GetMyWorkList(uid uint64, sortType string, sikp, limit uint64, playTimeType int) ([]*VideoModel, int64, bool, error) { cond := bson.M{"publisherID": uid} switch playTimeType { case 1: cond["newsType"] = SP case 2: cond["newsType"] = SHORT case 3: cond["newsType"] = COVER case 4: cond["newsType"] = PIC default: cond["newsType"] = bson.M{"$in": []string{SP, SHORT, COVER, PIC}} } sort := bson.D{{Key: "createdAt", Value: -1}} switch sortType { case "hot": sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}} case "new", "": sort = bson.D{{Key: "reviewAt", Value: -1}} default: } // infos, _, total, hasNext, err := getPublishedVideoList(page, size, cond, sort) return getWorksList(int64(sikp), int64(limit), cond, sort) } // GetWorksSortList 获取排序作品---默认最多存在5个作品 func GetWorksSortList(uid uint64, status *int) ([]*VideoModel, int64, bool, error) { cond := bson.M{"publisherID": uid, "worksSort": bson.M{"$gt": 0}} if status != nil { cond["status"] = status } sort := bson.D{{Key: "worksSort", Value: 1}} return getWorksList(0, 5, cond, sort) } func GetWorksSortCount(uid uint64, status *int) (int64, error) { cond := bson.M{"publisherID": uid, "worksSort": bson.M{"$gt": 0}} if status != nil { cond["status"] = status } // 获取总条数 return coll(nil).Count(cond) } func UpdateWorksSort(uid uint64, new, old int) error { cond := bson.M{"publisherID": uid, "worksSort": old} if _, err := coll(nil).UpdateOne(cond, bson.M{"$set": bson.M{"worksSort": new}}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateWorksSort", table, "UpdateOne", err), log.Any("cond", cond), log.Any("new", new)) return err } return nil } // GetVideoListByLocation 根据位置ID获取视频帖子列表,按照置顶,力荐,置精,播放量,审核通过时间排序 func GetVideoListByLocation(locationID primitive.ObjectID, page, limit int64) ([]*VideoModel, error) { var out []*VideoModel filter := bson.M{"location": locationID, "status": 1, "newsType": SP} opts := options.Find().SetSkip((page - 1) * limit).SetLimit(limit). SetSort(bson.D{{Key: "isChoosen", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}) return out, coll(nil).Find(&out, filter, opts) } // GetImageListByLocation 根据位置ID获取图片帖子列表,按照置顶,力荐,置精,播放量,审核通过时间排序 func GetImageListByLocation(locationID primitive.ObjectID, page, limit int64) ([]*VideoModel, error) { var out []*VideoModel filter := bson.M{"location": locationID, "status": 1, "newsType": COVER} opts := options.Find().SetSkip((page - 1) * limit).SetLimit(limit). SetSort(bson.D{{Key: "isChoosen", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}) return out, coll(nil).Find(&out, filter, opts) } // GetAllVideoListByMapCond 根据条件获取所有视频,不分页! func GetAllVideoListByMapCond(cond map[string]interface{}, sort primitive.D) ([]*VideoModel, stderr.Code) { opts := options.FindOptions{} if sort != nil { opts.SetSort(sort) } var back []*VideoModel if err := coll(nil).Find(&back, bson.M(cond), &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), log.Any("cond", cond), log.Any("sort", sort), ) return nil, stderr.ErrDbQueryError } return back, stderr.Success } // GetvideoListByMapCond web端通过组合条件获取视频列表 func GetvideoListByMapCond(cond map[string]interface{}, sort primitive.D, page, size uint64) ([]*VideoModel, int64, error) { infos, total, _, err := getVideoList(page, size, bson.M(cond), sort) return infos, total, err } // IncForwardCount 转发次数增加 func IncForwardCount(inc int, ids ...ObjectID) error { var query = bson.M{} if len(ids) == 1 { query["_id"] = ids[0] } else { query["_id"] = bson.M{"$in": ids} } update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"forwardCount": inc, "collectCount": inc}} _, err := coll(nil).UpdateOne(query, update) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), log.Any("ids", ids), ) return err } return nil } // TotalWorks 用户总作品数 func TotalWorks(filter bson.M) ([]WorkCount, error) { pip := []bson.M{ {"$match": filter}, // 过滤条件 由外部决定 {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, // 统计用户作品总数 {"$sort": bson.M{"count": -1}}, // 按照作品数排序 //{"$project": bson.M{"uid": "$_id.publisherID", "count": 1, "_id": 0}}, // 输出结果不包含_id } data := make([]WorkCount, 0) if err := coll(nil).Aggregate(&data, pip); err != nil { return nil, fmt.Errorf("TotalWorks err: %s", err.Error()) } return data, nil } // IncBatchForwardCount 转发次数增加 func IncBatchForwardCount(ids []ObjectID) error { query := bson.M{"_id": bson.M{"$in": ids}} update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"forwardCount": 1}} _, err := coll(nil).UpdateMany(query, update) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncBatchForwardCount", table, "UpdateMany", err), log.Any("ids", ids), ) return err } return nil } // GetPayVidList GetPayVidList func GetPayVidList(sort bson.D, skip int64, limit int64) ([]ObjectID, error) { filter := bson.M{ "status": CheckPass, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "newsType": SP, "coins": bson.M{"$gt": 0}, } opt := &options.FindOptions{} if len(sort) != 0 { opt.SetSort(sort) } opt.SetSkip(skip) opt.SetLimit(limit) payVideoList := make([]VideoModel, 0, limit) if err := coll(nil).Find(&payVideoList, filter, opt); err != nil { return nil, err } vidList := make([]ObjectID, len(payVideoList)) for i, v := range payVideoList { vidList[i] = v.ID } return vidList, nil } // GetFreeVidList 获取免费视频 func GetFreeVidList(sort bson.D, skip int64, limit int64) ([]ObjectID, error) { filter := bson.M{ "freeArea": true, "coins": 0, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } opt := &options.FindOptions{} opt.SetSort(bson.D{{Key: "fakePlayCount", Value: -1}, {Key: "createdAt", Value: -1}}) opt.SetSkip(skip) opt.SetLimit(limit) list := make([]VideoModel, 0, limit) if err := coll(nil).Find(&list, filter, opt); err != nil { return nil, err } listLen := len(list) if listLen == 0 { return []ObjectID{}, nil } vidList := make([]ObjectID, listLen) for i := range list { vidList[i] = list[i].ID } return vidList, nil } // GetVideoInfo 通过视频id获取视频详细信息 func GetVideoInfo(videoID string) (VideoModel, error) { v := VideoModel{} oid, err := primitive.ObjectIDFromHex(videoID) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoInfo", table, "ObjectIDFromHex", err), log.Any("videoID", videoID), ) return v, err } if err := coll(nil).FindOne(&v, bson.M{"_id": oid}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoInfo", table, "FindOne", err), log.Any("videoID", videoID), ) return v, err } return v, nil } // IsPublisher 判断是否是视频发布者 func IsPublisher(uid uint64, vid ObjectID) (bool, error) { info, err := GetVideoInfo(vid.Hex()) if err != nil { return false, err } return info.PublisherID == uid, nil } // GetShareCount 获取视频分享数 func GetShareCount(vid string) (int, error) { info, err := GetVideoInfo(vid) if err != nil { return 0, err } return info.ShareCount, nil } // GetShareList 获取分享视频列表 func getShareList(status *int) ([]ShareInfo, error) { filter := bson.M{"shareSort": bson.M{"$gt": 0}} if status != nil { filter["status"] = *status } opts := options.Find().SetSort(bson.D{{Key: "shareSort", Value: -1}}).SetLimit(20) var sis []ShareInfo return sis, coll(nil).Find(&sis, filter, opts) } // GetVideoTag 获取视频标签 func GetVideoTag(vid string) ([]ObjectID, error) { info, err := GetVideoInfo(vid) if err != nil { return nil, err } return info.Tags, nil } // GetVideoCover 获取视频封面 func GetVideoCover(vid string) (string, error) { info, err := GetVideoInfo(vid) if err != nil { return "", err } return info.Cover, nil } // FakeLikeGteLike 是否假点赞数大于真点赞数 func FakeLikeGteLike(vid string, fakeLike int) (bool, error) { info, err := GetVideoInfo(vid) if err != nil { return false, err } return fakeLike >= info.LikeCount, nil } // GetVidStatus 获取视频状态 func GetVidStatus(vid string) (int, error) { info, err := GetVideoInfo(vid) if err != nil { return 0, err } return info.Status, nil } // GetVideoCntByPublisher 获取某人发布的视频数 func GetVideoCntByPublisher(uid uint64, t string) (int64, error) { total, err := coll(nil).Count(bson.M{"publisherID": uid, "newsType": t}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoCntByPublisher", table, "Count", err), log.Any("uid", uid), ) return 0, err } return total, nil } // GetPublishedVideoCntByPublisher 获取某人过审发布的视频数 func GetPublishedVideoCntByPublisher(uid uint64) (int64, error) { total, err := coll(nil).Count(bson.M{"publisherID": uid, "status": 1}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPublishedVideoCntByPublisher", table, "Count", err), log.Any("uid", uid), ) return 0, err } return total, nil } // VideoListByTagID VideoListByTagID func VideoListByTagID(tid ObjectID, sort bson.D, vidSkipPer int64, vidLimitPer int64) ([]VideoModel, error) { filter := M{ "tags": M{ "$in": A{tid}, }, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } opt := &options.FindOptions{} if len(sort) != 0 { opt.SetSort(sort) } opt.SetSkip(vidSkipPer) opt.SetLimit(vidLimitPer) list := make([]VideoModel, 0, vidLimitPer) return list, coll(nil).Find(&list, filter, opt) } // VideoListByTagIDSort 排序 func VideoListByTagIDSort(tid ObjectID, newsType string, sort int, skip, limit uint64) (list []RecoVideoModel, err error) { filter := M{ "tags": tid, "newsType": newsType, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } sortCond := bson.D{{Key: "tagSort." + tid.Hex(), Value: -1}, {Key: "reviewAt", Value: -1}} switch sort { case 1: sortCond = bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} case 2: sortCond = bson.D{{Key: "reviewAt", Value: -1}} case 3: sortCond = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} case 4: sortCond = bson.D{{Key: "isChoosen", Value: -1}, {Key: "chosen", Value: -1}, {Key: "commentCount", Value: -1}, {Key: "reviewAt", Value: -1}} case 5: sortCond = bson.D{{Key: "reviewAt", Value: -1}} filter["playTime"] = bson.M{"$gte": 600} filter["newsType"] = SP default: } opt := options.Find().SetSkip(int64(skip)).SetLimit(int64(limit)) opt.SetProjection(bson.M{"chosenDate": 1, "createdAt": 1}) // 默认展示_id字段 opt.SetSort(sortCond) //内嵌动态字段无法设置索引,建议不要这么设计 return list, coll(nil).Find(&list, filter, opt) } // VideoListByTagIDForRecommend 根据标签推荐 func VideoListByTagIDForRecommend(tid, NewsType, quality, direction string, line time.Time, vidPage, vidLimitPer uint64) ([]string, time.Time, error) { oid, _ := primitive.ObjectIDFromHex(tid) filter := M{"tags": oid, "newsType": NewsType, "createdAt": bson.M{"$gt": line}, "coins": 0, "status": 1} //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 if quality != "" { filter["quality"] = quality } if direction != "" { filter["direction"] = direction } sort := bson.D{{Key: "createdAt", Value: 1}} ids, _, maxCreateLine, err := getDistinctVideoID(vidPage, vidLimitPer, filter, sort) if err != nil { return nil, maxCreateLine, err } vidIDs := common.ObjectIDs2String(ids) return vidIDs, maxCreateLine, nil } // InsertBase 提交视频基本信息 func InsertBase(info VideoModel) (primitive.ObjectID, error) { vid, err := coll(nil).InsertOne(&info) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertBase", table, "InsertOne", err), log.Any("info", info), ) return primitive.NilObjectID, err } return vid.InsertedID.(primitive.ObjectID), nil } // 根据md5判断该视频是否重复 func IsExist(md5 string, size int) bool { suc, err := coll(nil).Exists(bson.M{"md5": md5, "size": size}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExist", table, "Exists", err), log.Any("md5", md5), log.Any("size", size), ) return suc } return suc } // 根据sourceID 判断文件视频文件是否存在 func IsExistBySocID(sourceID string) bool { suc, err := coll(nil).Exists(bson.M{"sourceID": sourceID}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExistBySocID", table, "Exists", err), log.Any("sourceID", sourceID), ) return suc } return suc } // GetFreeTime 获取免费时长 func GetFreeTime(tm uint) int { if tm < second30 { return 0 } if tm < minite10 { return free10 } if tm < minite30 { return free60 } if tm < minite60 { return free120 } return free120 } // IncCommentCount 评论统计次数加加 func IncCommentCount(id ObjectID) error { query := bson.M{"_id": id} update := bson.M{"$inc": bson.M{"commentCount": 1, "fakeCommentCount": 1}} if _, err := coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCommentCount", table, "UpdateOne", err), log.Any("id", id), ) return err } return nil } // IncCommentCount 评论统计次数减少 func DecCommentCount(id ObjectID, n int64) error { query := bson.M{"_id": id} update := bson.M{"$inc": bson.M{"commentCount": n, "fakeCommentCount": n}} if _, err := coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DecCommentCount", table, "UpdateOne", err), log.Any("id", id), ) return err } return nil } // IncLikeCount 点赞统计次数加加 func IncLikeCount(id ObjectID) error { query := bson.M{"_id": id} update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": 1, "fakeLikeCount": 1}} if _, err := coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), log.Any("id", id), ) return err } return nil } // DecLikeCount 点赞统计次数减减 func DecLikeCount(ids ...ObjectID) error { var query = bson.M{} if len(ids) == 1 { query["_id"] = ids[0] } else { query["_id"] = bson.M{"$in": ids} } update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": -1, "fakeLikeCount": -1}} if _, err := coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DecLikeCount", table, "UpdateOne", err), log.Any("ids", ids), ) return err } return nil } // IncPlayCount 播发统计次数加加 func IncPlayCount(id ObjectID, req PlayReq, popCfg vidpopmod.VideoPopularityConfig) error { query := bson.M{"_id": id} vid, err := GetVideoInfo(id.Hex()) if err != nil { log.Error("IncPlayCount GetVideoInfo", log.Any("video id", id), log.E(err)) return err } now := time.Now() update := bson.M{"$set": bson.M{"updatedAt": now}, "$inc": bson.M{"playCount": 1, "fakePlayCount": 1}} if req.Longer > vid.FreeTime { vid.EffectivePlayCount += calcVideoEffectivePlayCount(req.Longer, int(vid.PlayTime), popCfg) vid.PlayCount += 1 vid.FakePlayCount += 1 update = bson.M{ "$set": bson.M{ "updatedAt": now, //"hot": calcHot(&vid, &popCfg), }, "$inc": bson.M{ "playCount": 1, "fakePlayCount": 1, "effectivePlayCount": calcVideoEffectivePlayCount(req.Longer, int(vid.PlayTime), popCfg), }} } if _, err = coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPlayCount", table, "UpdateOne", err), log.Any("id", id), ) return err } return nil } func calcVideoEffectivePlayCount(longer, total int, popCfg vidpopmod.VideoPopularityConfig) int { for _, v := range popCfg.PlayTimePercentage { if total >= v.Min && total < v.Max { eff := float64(total) * float64(v.Percentage) / 100 if float64(longer) > eff { return 1 } } } return 0 } // IncPurchaseCount 购买次数增加 func IncPurchaseCount(id ObjectID) error { query := bson.M{"_id": id} update := bson.M{"$inc": bson.M{"purchaseCount": 1}} if _, err := coll(nil).UpdateOne(query, update); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchaseCount", table, "UpdateOne", err), log.Any("id", id), ) return err } return nil } // UpdateVideoByMapCond web通过map视频更新 func UpdateVideoByMapCond(id ObjectID, set bson.M) (VideoModel, error) { set["updatedAt"] = time.Now() var v VideoModel if err := coll(nil).FindOneAndUpdate(&v, bson.M{"_id": id}, bson.M{"$set": set}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoByMapCond", table, "UpdateMany", err), log.Any("id", id), log.Any("update", set), ) return v, err } return v, nil } func UpdateOneByID(id ObjectID, set bson.M) (res *mongo.UpdateResult, err error) { return coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) } func BatchUpdateReco(ids []ObjectID, reco bool) error { if len(ids) == 0 { return nil } query := bson.M{"_id": bson.M{"$in": ids}} var update = bson.M{} if reco { update["recoWeight"] = 0 } else { update["recoWeight"] = -1 } _, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) return err } // UpdateVideoBatch web批量更新视频状态 func UpdateVideoBatch(ids []ObjectID, field string, status bool) (int64, error) { if len(ids) == 0 { return 0, nil } query := bson.M{"_id": bson.M{"$in": ids}} update := bson.M{field: status} now := time.Now() if field == "chosen" { update["chosenDate"] = now } if field == "freeArea" { update["freeAreaDate"] = now update["recoWeight"] = -1 } update["updatedAt"] = now //if field == "liaoBaTop" && status { // n, err := CountNum(bson.M{"liaoBaTop": true}) // if err != nil { // return 0, err // } //if n >= 20 { // return 0, errors.New("overLiaoBaLimit") //} //} result, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoBatch", table, "UpdateMany", err), log.Any("ids", ids), log.Any("field", field), log.Any("status", status), ) return 0, err } return result.ModifiedCount, nil } // UpdateManyVideo web批量更新视频状态 func UpdateManyVideo(ids []ObjectID, update bson.M) (int64, error) { if len(ids) == 0 { return 0, nil } query := bson.M{"_id": bson.M{"$in": ids}} result, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateManyVideo", table, "UpdateMany", err), log.Any("ids", ids), ) return 0, err } return result.ModifiedCount, nil } // UpdateVideosByMapCond web通过map视频更新 func UpdateVideosByMapCond(ids []ObjectID, set bson.M) (err error) { set["updatedAt"] = time.Now() if _, err = coll(nil).UpdateMany(bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": set}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoByMapCond", table, "UpdateMany", err), log.Any("ids", ids), log.Any("update", set), ) return err } return nil } // PassVids web批量更新视频状态 func PassVidsOnlineTime(id primitive.ObjectID, pass int, reason string, manager string, onlineTime time.Time, tagSort primitive.M) (VideoModel, error) { var vid VideoModel query := bson.M{"_id": id} var update bson.M now := time.Now() if pass == Free { update = bson.M{"status": CheckPass, "coins": 0, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager} } else if pass == CheckPass { update = bson.M{"status": CheckPass, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager, "tagSort": tagSort} } else if pass == CheckFailure { update = bson.M{"status": CheckFailure, "reason": reason, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager} } if err := coll(nil).FindOneAndUpdate(&vid, query, bson.M{"$set": update}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PassVids", table, "FindOneAndUpdate", err), log.Any("id", id), log.Any("pass", pass), log.Any("reason", reason), ) return vid, err } return vid, nil } // PassVids web批量更新视频状态 func PassVids(id primitive.ObjectID, pass int, reason string, manager string, tagSort primitive.M) (VideoModel, error) { var vid VideoModel query := bson.M{"_id": id} var update bson.M now := time.Now() if pass == Free { update = bson.M{"status": CheckPass, "coins": 0, "updatedAt": now, "reviewAt": now, "reviewAccount": manager} } else if pass == CheckPass { update = bson.M{"status": CheckPass, "updatedAt": now, "reviewAt": now, "reviewAccount": manager, "tagSort": tagSort} } else if pass == CheckFailure { update = bson.M{"status": CheckFailure, "reason": reason, "updatedAt": now, "reviewAt": now, "reviewAccount": manager} } if err := coll(nil).FindOneAndUpdate(&vid, query, bson.M{"$set": update}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PassVids", table, "FindOneAndUpdate", err), log.Any("id", id), log.Any("pass", pass), log.Any("reason", reason), ) return vid, err } return vid, nil } // UpdateVideoResolutionPlayTime 更新视频 func UpdateVideoResolutionPlayTime(id ObjectID, update WebVideoUpdateDoc) (int64, error) { query := bson.M{"_id": id} doc, _ := common.ToBsonM(update) result, err := coll(nil).UpdateOne(query, bson.M{"$set": doc}) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoResolutionPlayTime", table, "UpdateOne", err), log.Any("id", id), log.Any("update", update), ) return 0, err } return result.ModifiedCount, nil } // sumFakePlayCount sumFakePlayCount func sumFakePlayCount(sort D, skip, limit int64, mats ...pageopt.Matcher) (int64, error) { videoList := make([]VideoModel, 0, limit) opt := (&options.FindOptions{}). SetSort(sort). SetSkip(skip). SetLimit(limit) filter := pageopt.MergeM(mats) filter["status"] = 1 //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 if err := coll(nil).Find(&videoList, filter, opt); err != nil { return 0, err } total := int64(0) for _, v := range videoList { total += int64(v.FakePlayCount) } return total, nil } func vidList(sort D, skip, limit int64) ([]ObjectID, error) { videoList := make([]VideoModel, 0, limit) opt := (&options.FindOptions{}). SetSort(sort). SetSkip(skip). SetLimit(limit) filter := M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 } if err := coll(nil).Find(&videoList, filter, opt); err != nil { return nil, err } vidList := make([]ObjectID, 0, len(videoList)) for _, video := range videoList { vidList = append(vidList, video.ID) } return vidList, nil } // VIDListByCreatedTime VIDListByCreatedTime func VIDListByCreatedAt(skip, limit int64) ([]ObjectID, error) { sort := D{ {Key: "createdAt", Value: -1}, } return vidList(sort, skip, limit) } func SumFakePlayCountSortByCreatedAt(limit int64) (int64, error) { sort := D{ {Key: "createdAt", Value: -1}, } return sumFakePlayCount(sort, 0, limit) } func SumFakePlayCountSortByFakeLikeCount(limit int64) (int64, error) { sort := D{ {Key: "fakeLikeCount", Value: -1}, } return sumFakePlayCount(sort, 0, limit) } // VIDListByFakePlayCount VIDListByFakePlayCount func VIDListByFakePlayCount(skip, limit int64) ([]ObjectID, error) { sort := D{ {Key: "fakePlayCount", Value: -1}, } return vidList(sort, skip, limit) } func SumFakePlayCountSortByFakePlayCount(limit int64) (int64, error) { sort := D{ {Key: "fakePlayCount", Value: -1}, } return sumFakePlayCount(sort, 0, limit) } // VIDListByFakeLikeCount VIDListByFakeLikeCount func VIDListByFakeLikeCount(skip, limit int64) ([]ObjectID, error) { sort := D{ {Key: "fakeLikeCount", Value: -1}, } return vidList(sort, skip, limit) } func SumFakePlayCountByFakeLikeCount(limit int64) (int64, error) { sort := D{ {Key: "fakeLikeCount", Value: -1}, } return sumFakePlayCount(sort, 0, limit) } // VIDListSortByFakeCommentCount VIDListSortByFakeCommentCount func VIDListSortByFakeCommentCount(skip, limit int64) ([]ObjectID, error) { sort := D{ {Key: "fakeCommentCount", Value: -1}, } return vidList(sort, skip, limit) } func SumFakePlayCountSortByFakeCommentCount(limit int64) (int64, error) { sort := D{ {Key: "fakeCommentCount", Value: -1}, } return sumFakePlayCount(sort, 0, limit) } func SumFakePlayCountSortByFreeArea(limit int64) (int64, error) { sort := D{{Key: "fakeCommentCount", Value: -1}} freeAreaMatch := &pageopt.AssignMatch{Key: "freeArea", Val: true} coinsMatch := &pageopt.AssignMatch{Key: "coins", Val: 0} return sumFakePlayCount(sort, 0, limit, freeAreaMatch, coinsMatch) } func VIDListToneOfficialRecom(skip, limit int64) ([]primitive.ObjectID, error) { sort := D{ {Key: "fakeCommentCount", Value: -1}, } opt := (&options.FindOptions{}). SetSort(sort). SetSkip(skip). SetLimit(limit) filter := M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "chosen": true, } var videoList []VideoModel if err := coll(nil).Find(&videoList, filter, opt); err != nil { return nil, err } vidList := make([]ObjectID, len(videoList)) for i := range videoList { vidList[i] = videoList[i].ID } return vidList, nil } func SumFakePlayCountSortByToneOfficialRecom(limit int64) (int64, error) { sort := D{ {Key: "fakeCommentCount", Value: -1}, } opt := (&options.FindOptions{}). SetSort(sort). SetLimit(limit) filter := M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "chosen": true, } videoList := make([]VideoModel, 0, limit) if err := coll(nil).Find(&videoList, filter, opt); err != nil { return 0, err } total := int64(0) for _, v := range videoList { total += int64(v.FakePlayCount) } return total, nil } // 通过关键字或tid匹配title获取VID List func VIDListByKeywordOrTags(keyword string, tids []ObjectID, skip int64, limit int64) ([]ObjectID, error) { opt := (&options.FindOptions{}). SetSkip(skip). SetLimit(limit). SetSort(D{{Key: "fakePlayCount", Value: -1}, {Key: "createdAt", Value: -1}}) //索引 因为业务没有明确要求顺序 排序放最后提升效率 filter := M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "title": M{ "$regex": fmt.Sprintf("^%s", keyword), }, } if len(tids) != 0 { filter = M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "$or": A{ M{"title": M{ "$regex": fmt.Sprintf("^%s", keyword), }}, M{"tags": M{"$in": tids}}, }, } } docList := make([]struct { ID ObjectID `bson:"_id"` }, 0, limit) if err := coll(nil).Find(&docList, filter, opt); err != nil { return nil, err } idList := make([]ObjectID, len(docList)) for i := range docList { idList[i] = docList[i].ID } return idList, nil } // GetCityPlayCount2Map 获取城市的访问量和播放量(热度) func GetCityPlayCount2Map(locIDs []ObjectID) (map[ObjectID]int, error) { if locIDs == nil { locIDs = []ObjectID{} } m := make(map[ObjectID]int) var data []CityCount p := []bson.M{ {"$match": bson.M{"location": bson.M{"$in": locIDs}, "status": 1}}, {"$group": bson.M{"_id": "$location", "count": bson.M{"$sum": "$playCount"}}}, } if err := coll(nil).Aggregate(&data, p); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCityPlayCount2Map", table, "Aggregate", err), log.Any("locIDs", locIDs), ) return nil, err } for _, d := range data { m[d.ID] = d.Count } return m, nil } // GetCityPublishedVideoCount2Map 获取城市的视频数 func GetCityPublishedVideoCount2Map(locIDs []ObjectID) (map[ObjectID]int, error) { if locIDs == nil { locIDs = []ObjectID{} } m := make(map[ObjectID]int) var data []CityCount p := []bson.M{ {"$match": bson.M{"location": bson.M{"$in": locIDs}, "status": 1}}, {"$group": bson.M{"_id": "$location", "count": bson.M{"$sum": 1}}}, } if err := coll(nil).Aggregate(&data, p); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCityPublishedVideoCount2Map", table, "Aggregate", err), log.Any("locIDs", locIDs), ) return nil, err } for _, d := range data { m[d.ID] = d.Count } return m, nil } // GetTitleTimeByIDs 获取视频标题 func GetTitleTimeByIDs(videoIDs []primitive.ObjectID) (map[primitive.ObjectID]TitleTime, error) { m := make(map[primitive.ObjectID]TitleTime) if videoIDs == nil { videoIDs = []primitive.ObjectID{} } infos, err := GetVideoListByIDs(videoIDs) if err != nil { return m, err } for _, i := range infos { info := TitleTime{ Title: i.Title, PlayTime: i.PlayTime, } m[i.ID] = info } return m, nil } // uid获取视频并按播放量降序 func GetVideosByPublisherID(publisherID uint64, stdQuery commod.StdQuery) (data []VideoModel, err error) { if err = coll(nil).Find(&data, bson.M{"publisherID": publisherID}, commod.ConvertToListQuery(stdQuery)); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideosByPublisherID", table, "Find", err), log.Any("publisherID", publisherID), ) return } return } func CountPayVidsByUIDS(ids []primitive.ObjectID) (total int64, err error) { total, err = coll(nil).Count(bson.M{"_id": bson.M{"$in": ids}}) if err != nil { log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountPayVidsByUIDS", table, "Count", err), log.Any("ids", ids), ) return } return } // RecommendVidoeByCity 通过城市推荐视频 func RecommendVidoeByCity(locID primitive.ObjectID, line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { cond := bson.M{"location": locID, "coins": 0, "createdAt": bson.M{"$gt": line}, "newsType": newsType} if quality != "" { cond["quality"] = quality } if direction != "" { cond["direction"] = direction } sort := bson.D{{Key: "createdAt", Value: 1}} ids, _, maxLine, err := getDistinctVideoID(page, size, cond, sort) if err != nil { return nil, maxLine, err } arr := common.ObjectIDs2String(ids) return arr, maxLine, err } // RecommendVidoeByHotWords 通过热词推荐视频 func RecommendVidoeByHotWords(words []string, page, size uint64) []string { pt := make([]string, len(words)) for _, w := range words { es, err := Search(w, int64((page-1)*size), int64(size)) if err != nil { log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RecommendVidoeByHotWords", table, "Search", err), log.Any("words", words), log.Any("page", page), log.Any("size", size), log.E(err)) continue } for _, v := range es { pt = append(pt, v.ID.Hex()) } } return pt } // GetChosenVideoID 获取精选视频 func GetChosenVideoID(line time.Time, newsType string, page, size uint64) ([]string, time.Time, error) { cond := bson.M{"coins": 0, "newsType": newsType} sort := bson.D{{Key: "createdAt", Value: 1}} ids, maxLine, err := getChosenVideoID(line, page, size, cond, sort) if err != nil { return []string{}, maxLine, err } arr := common.ObjectIDs2String(ids) return arr, maxLine, err } // GetPublishedVideoListForSyncCdn 条件获取视频列表 分页获取 func GetPublishedVideoListForSyncCdn(page int, size int, lastId string) ([]*VideoModel, error) { cond := bson.M{} cond["status"] = 1 if lastId != "" { oid, _ := primitive.ObjectIDFromHex(lastId) cond["_id"] = bson.M{"$gt": oid} } opts := options.FindOptions{} opts.SetSort(bson.D{{Key: "createdAt", Value: 1}}) opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size)) var back []*VideoModel if err := coll(nil).Find(&back, cond, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPublishedVideoListForSyncCdn", table, "Find", err), log.Any("page", page), log.Any("size", size), log.Any("lastId", lastId), ) return nil, err } return back, nil } func GetVideoListByUpdateTimeRange(start time.Time, end time.Time, page int, size int) (data []*VideoModel, hasNext bool, err error) { var query = bson.M{ "updatedAt": bson.M{"$gte": start, "$lt": end}, } opts := options.FindOptions{} opts.SetSort(bson.D{{Key: "_id", Value: 1}}) opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size) + 1) if err = coll(nil).Find(&data, query, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByUpdateTimeRange", table, "Find", err), log.Any("start", start), log.Any("end", end), ) return } if len(data) > size { hasNext = true data = data[:size] } return } func GetVideoListLtMinPlayLikeCount(minFakeLikesCnt, minFakePlayCnt, skip, limit int64) (data []*VideoModel, err error) { filter := M{ "$or": A{ M{"fakeLikeCount": M{"$lt": minFakeLikesCnt}}, M{"fakePlayCount": M{"$lt": minFakePlayCnt}}, }, } opts := options.Find().SetSkip(skip).SetLimit(limit) err = coll(nil).Find(&data, filter, opts) return } func GetVideoListByCreateTimeRange(start time.Time, end time.Time, maxCommentNum int) (data []*VideoModel, err error) { var query = bson.M{ "createdAt": bson.M{"$gte": start, "$lt": end}, "commentCount": bson.M{"$lt": maxCommentNum}, "status": 1, } opts := options.Find().SetSkip(0).SetLimit(5000) if err = coll(nil).Find(&data, query, opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByCreateTimeRange", table, "Find", err), log.Any("start", start), log.Any("end", end), ) return } return } func FindMany(mats ...Matcher) (VIDSlice, error) { return List(nil, nil, nil, mats...) } // GetHisVideoCount 获取多个用户视频总数 func GetHisVideoCount(uids []uint64) (map[uint64]int, error) { if uids == nil { uids = []uint64{} } var data []UserVideoCount p := []bson.M{ {"$match": bson.M{"publisherID": bson.M{"$in": uids}, "status": 1}}, {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, } if err := coll(nil).Aggregate(&data, p); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHisVideoCount", table, "Aggregate", err), log.Any("uids", uids), ) return nil, err } m := make(map[uint64]int) for _, d := range data { m[d.ID] = d.Count } return m, nil } // 获取新导入的视频 func GetNewImportVideo(newUpdatedAt string, startTime, endTime time.Time, page, pageSize int64) (data []*VideoModel, err error) { opts := options.Find().SetSkip((page - 1) * pageSize).SetLimit(pageSize).SetSort(bson.D{{Key: "createdAt", Value: 1}}) query := bson.M{} if newUpdatedAt != "" { query["newUpdatedAt"] = newUpdatedAt } if !startTime.IsZero() { query["createdAt"] = bson.M{"$gte": startTime} } if !endTime.IsZero() { query["createdAt"] = bson.M{"$lt": endTime} } if err = coll(nil).Find(&data, query, opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNewImportVideo", table, "Find", err), log.Any("newUpdatedAt", newUpdatedAt), log.Any("startTime", startTime), log.Any("endTime", endTime), ) return } return } func UpdateManyForContentCount(m map[primitive.ObjectID]int) (updateCount int64, err error) { models := make([]mongo.WriteModel, len(m)) i := 0 for k, v := range m { models[i] = mongo.NewUpdateOneModel().SetFilter(bson.M{"_id": k}).SetUpdate(bson.M{"$inc": bson.M{"commentCount": v}}) i++ } res, err := coll(nil).Bulk(models) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateMany", table, "Bulk", err), log.Any("map", m)) return } updateCount = res.ModifiedCount return } // DelVideo 删除视频 func DelVideo(ids []primitive.ObjectID) error { if ids == nil { return nil } if _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "err", err), log.Any("ids", ids)) return err } return nil } // GetVideosByUIDs 通过uids获取视频 func GetVideosByUIDs(ids []uint64) (map[uint64]int, map[uint64]int, error) { mTotal := make(map[uint64]int) mUndeal := make(map[uint64]int) if ids == nil { return mTotal, mUndeal, nil } var vInfos []VideoModel if err := coll(nil).Find(&vInfos, bson.M{"publisherID": bson.M{"$in": ids}}); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideosByUIDs", table, "err", err), log.Any("ids", ids)) return mTotal, mUndeal, err } for _, v := range vInfos { if v.Status == 0 { mUndeal[v.PublisherID]++ } mTotal[v.PublisherID]++ } return mTotal, mUndeal, nil } // getPublishedNewsList 条件获取帖子列表 func getPublishedNewsList(page, size uint64, cond bson.M, sort bson.D) ([]*VideoModel, bool, error) { hasNext := false cond["status"] = 1 skip := (page - 1) * size opts := options.FindOptions{} if sort != nil { opts.SetSort(sort) } opts.SetSkip(int64(skip)).SetLimit(int64(size + 1)) var back []*VideoModel if err := coll(nil).Find(&back, cond, &opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err), log.Any("page", page), log.Any("size", size), log.Any("cond", cond), log.Any("sort", sort), ) return nil, hasNext, err } if uint64(len(back)) > size { hasNext = true back = back[:size] } return back, hasNext, nil } // GetNewestNews 获取最新帖子列表 func GetNewestNews(page, size uint64, recentMinute time.Time) ([]*VideoModel, bool, error) { cond := bson.M{"reviewAt": bson.M{"$lte": recentMinute}} sort := bson.D{{Key: "reviewAt", Value: -1}} return getPublishedNewsList(page, size, cond, sort) } // GetNewestNews 获取最新帖子列表 func GetNewestNews_old(page, size uint64) ([]*VideoModel, bool, error) { cond := bson.M{} sort := bson.D{{Key: "reviewAt", Value: -1}} return getPublishedNewsList(page, size, cond, sort) } // GetNewestNews 获取最新帖子列表 func GetNewestShortVideo(page, size uint64) ([]*VideoModel, bool, error) { cond := bson.M{"newsType": SP, "playTime": bson.M{"$lte": 600}} sort := bson.D{{Key: "reviewAt", Value: -1}} return getPublishedNewsList(page, size, cond, sort) } // GetList 获取帖子列表 func GetList(filter primitive.M, opts ...*options.FindOptions) (out []*VideoModel, err error) { if err = coll(nil).Find(&out, filter, opts...); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Find", err), log.Any("filter", filter), log.Any("opts", opts), ) return } return } // GetNewsByIDs2M 通过id获取视频信息到map func GetNewsByIDs2M(ids []primitive.ObjectID) (map[primitive.ObjectID]*VideoModel, error) { mInfo := make(map[primitive.ObjectID]*VideoModel) cond := bson.M{"_id": bson.M{"$in": ids}} infos, _, err := getPublishedNewsList(1, uint64(len(ids)), cond, nil) if err != nil { return mInfo, err } for _, v := range infos { mInfo[v.ID] = v } return mInfo, err } // GetNewsListLocation 获取附近的帖子列表 func GetNewsListLocation(lid ObjectID, page, size uint64) ([]*VideoModel, bool, error) { cond := bson.M{"location": lid} sort := bson.D{{Key: "playCount", Value: -1}, {Key: "createdAt", Value: -1}} return getPublishedNewsList(page, size, cond, sort) } // GetCoinsNews 获取金币专区帖子列表 func GetCoinsNews(page, size uint64, typ int) ([]*VideoModel, bool, error) { cond := bson.M{"coins": bson.M{"$gt": 0}} if typ == CommonUp { cond["$or"] = bson.A{bson.M{"isMadou": bson.M{"$exists": false}}, bson.M{"isMadou": false}} } if typ == MadouUp { cond["isMadou"] = true } sort := bson.D{{Key: "createdAt", Value: -1}} return getPublishedNewsList(page, size, cond, sort) } // 获取原创新版帖子列表 func GetOriginals(uid uint64) ([]*VideoModel, error) { cond := bson.M{"publisherID": uid, "newsType": SP} sort := bson.D{{Key: "worksSort", Value: -1}} infos, _, err := getPublishedNewsList(1, 3, cond, sort) return infos, err } // GetTotalNewsList 统计获取帖子 func GetTotalNewsList() ([]*VideoModel, error) { cond := bson.M{"status": 1} var back []*VideoModel if err := coll(nil).Find(&back, cond); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err)) return back, err } return back, nil } // IsNewsCover 获取帖子类型 func IsNewsCover(id string) (bool, error) { info, err := GetVideoInfo(id) if err != nil { return false, err } return info.NewsType == COVER || info.NewsType == SEED_LINK || info.NewsType == PIC || info.MDSID != "", nil } // GetTopping 获取置顶 func GetTopping(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { cond := bson.M{"isTopping": true} sort := bson.D{{Key: "reviewAt", Value: -1}} if typ == TypeSameCity { cond["location"] = lid } if typ == TypePay { cond["coins"] = bson.M{"$gt": 0} cond["isMadou"] = isMadou } infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) return infos, err } // GetReco 获取力荐 func GetReco(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { cond := bson.M{"isRecommend": true} sort := bson.D{{Key: "reviewAt", Value: -1}} if typ == TypeSameCity { cond["location"] = lid } if typ == TypePay { cond["coins"] = bson.M{"$gt": 0} cond["isMadou"] = isMadou } infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) return infos, err } // GetChosen 获取置精 func GetChosen(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { cond := bson.M{"isChoosen": true} sort := bson.D{{Key: "reviewAt", Value: -1}} if typ == TypeSameCity { cond["location"] = lid } if typ == TypePay { cond["coins"] = bson.M{"$gt": 0} cond["isMadou"] = isMadou } infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) return infos, err } // IsSP 是否是短视频 func IsSP(newsType string) bool { return newsType == SP } // IsCover 是否是图片帖子 func IsCover(newsType string) bool { return newsType == COVER || newsType == PIC } // IncRewarded 增加打赏金额 func IncRewarded(vid primitive.ObjectID, decimal decimal.Decimal) error { cond := bson.M{"_id": vid} update := bson.M{"$inc": bson.M{"rewarded": decimal}} _, err := coll(nil).UpdateOne(cond, update) return err } // IncSpecFakeRewarded 增加指定的(假)打赏金额 func IncSpecFakeRewarded(vid primitive.ObjectID, decimal decimal.Decimal) error { cond := bson.M{"_id": vid} update := bson.M{"$inc": bson.M{"fakeRewarded": decimal}} _, err := coll(nil).UpdateOne(cond, update) return err } func GetManyPrefetchVideos(page, pageSize int64) ([]*PrefetchVideoModel, error) { opt := options.Find(). SetLimit(pageSize). SetSkip((page - 1) * pageSize). SetSort(bson.M{"createdAt": 1}) var data []*PrefetchVideoModel return data, coll(nil).Find(&data, bson.M{"status": 1}, opt) } func GetLiaoBaTop() ([]*VideoModel, error) { var data []*VideoModel opts := options.Find().SetSort(bson.D{ {Key: "liaoBaTopSort", Value: -1}, {Key: "reviewAt", Value: -1}, {Key: "_id", Value: -1}, }) return data, coll(nil).Find(&data, bson.M{"liaoBaTop": true, "status": bson.M{"$nin": []int{0, 2}}}, opts) } func GetVipList(page, pageSize uint64) ([]*VideoModel, bool, error) { cond := bson.M{"coins": 0} sort := bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "reviewAt", Value: -1}} return getPublishedNewsList(page, pageSize, cond, sort) } // FindShortVidByIDs 获取长视频 func FindShortVidByIDs(vids []primitive.ObjectID) ([]primitive.ObjectID, error) { var data struct { IDS []primitive.ObjectID `bson:"ids"` } p := []bson.M{ {"$match": bson.M{"_id": bson.M{"$in": vids}, "playTime": bson.M{"$gt": 600}}}, {"$group": bson.M{"_id": nil, "ids": bson.M{"$push": "$_id"}}}, } if err := coll(nil).AggregateDecode(&data, p); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindShortVidByIDs", table, "Aggregate", err), log.Any("vids", vids), ) return nil, err } return data.IDS, nil } // GetRecommIDs 获取推荐视频 func GetRecommIDs(limit int64) ([]*VideoModel, error) { cond := bson.M{"newsType": SP, "status": 1, "quality": bson.M{"$in": []string{Middle, High}}} var back []*VideoModel opt := options.Find().SetLimit(limit) if err := coll(nil).Find(&back, cond, opt); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoIDsList", table, "AggregateDecode", err), log.Any("cond", cond), ) return nil, err } return back, nil } // GetHotVideoByOnlineTimeRange 获取时间区间上架的最热视频 func GetHotVideoByOnlineTimeRange(start time.Time, end time.Time, page, size int64) (back []*VideoModel, hasNext bool, err error) { var query = bson.M{ "newsType": SP, "reviewAt": bson.M{"$gte": start, "$lt": end}, "status": CheckPass, } opt := &options.FindOptions{} opt.SetSort(bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}).SetSkip((page - 1) * size).SetLimit(size + 1) if err = coll(nil).Find(&back, query, opt); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotVideoByOnlineTimeRange", table, "Find", err), log.Any("start", start), log.Any("end", end), ) return } if int64(len(back)) > size { hasNext = true back = back[:size] } return } func IncVideoPageView(vids []primitive.ObjectID) error { if len(vids) == 0 { return nil } query := bson.M{"_id": bson.M{"$in": vids}} update := bson.M{"$inc": bson.M{"pageViewCount": 1}} _, err := coll(nil).UpdateMany(query, update) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncVideoPageView", table, "UpdateMany", err), log.Any("vids", vids)) return err } return nil } func Bulk(models []mongo.WriteModel) error { _, err := coll(nil).Bulk(models, options.BulkWrite().SetOrdered(false)) return err } // GetCountByPublish 获取用户视频数量 status 0 未审核 1通过 2审核失败 3视为免费 默认为0 func GetCountByPublish(publisherID uint64, status *int) (int64, error) { var filter = bson.M{"publisherID": publisherID} if status != nil { filter["status"] = status } return coll(nil).Count(filter) } // WorkLeaderboard 作品榜单 func WorkLeaderboard(bind interface{}, filter bson.M, limit int) error { filter["publisherID"] = bson.M{"$nin": []uint64{100007, 100008}} // 排除官方上传 pip := []bson.M{ {"$match": filter}, // 过滤条件 由外部决定 {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, // 统计用户作品总数 {"$sort": bson.M{"count": -1}}, // 按照作品数排序 {"$limit": limit}, // 限制返回条数 } return coll(nil).Aggregate(bind, pip) } // GetCreatorNumber 获取创作者数量(查询非常慢,需要配合redis使用) func GetCreatorNumber() (int64, error) { var result []struct { Count int64 `json:"count" bson:"count"` } pip := []bson.M{ {"$group": bson.M{"_id": "$publisherID"}}, // 按照创作者ID分组 {"$group": bson.M{"_id": "null", "count": bson.M{"$sum": 1}}}, // 统计创作者总数 {"$project": bson.M{"_id": 0}}, // 输出结果不包含_id } if err := coll(nil).Aggregate(&result, pip); err != nil { return 0, err } // 容错判定 if len(result) > 0 { return result[0].Count, nil } return 0, nil } func IsSubmitByPublisherID(publisherID uint64) (bool, error) { return coll(nil).Exists(bson.M{"publisherID": publisherID}) } // GetRecommendation 获取推荐帖子==>幸福广场使用 func GetRecommendation() (out []*VideoModel, err error) { filter := bson.M{ "newsType": COVER, "status": 1, //"$or": bson.A{ // bson.M{"isTopping": true}, // bson.M{"isRecommend": true}, // bson.M{"isChoosen": true}, //}, } if err = coll(nil).Find(&out, filter, options.Find().SetLimit(10)); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRecommendation", table, "Find", err), log.Any("filter", filter), ) return nil, err } return } // VideoListByTagID func VideoListByTagIDAndPlayTime(tid ObjectID, playDuration int, outVids []ObjectID) ([]*VideoModel, error) { opt := &options.FindOptions{} opt.SetSort(bson.D{{Key: "playCount", Value: -1}}) filter := M{ "tags": tid, "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "coins": 0, } if playDuration == 1 { filter["playTime"] = bson.M{"$gte": 600} opt.SetLimit(1) } else if playDuration == 2 { filter["playTime"] = bson.M{"$lt": 600} opt.SetLimit(2) } if len(outVids) > 0 { filter["_id"] = bson.M{"$nin": outVids} } list := make([]*VideoModel, 0) return list, coll(nil).Find(&list, filter, opt) } // HotVideo 热门视频 func HotVideo() ([]*VideoModel, error) { opt := &options.FindOptions{} opt.SetSort(bson.D{{Key: "playCount", Value: -1}}).SetLimit(20) filter := M{ "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 "coins": 0, } list := make([]*VideoModel, 0) return list, coll(nil).Find(&list, filter, opt) } func GetVideosByIDs(ids []primitive.ObjectID) ([]*VideoModel, error) { var out []*VideoModel return out, coll(nil).Find(&out, bson.M{"_id": bson.M{"$in": ids}, "status": 1}) } // GetRecommendVideosByIDsContext 获取仍满足短视频推荐条件的视频完整快照。 // 队列生成后视频可能被下架、删除或改变类型,因此请求侧必须再次校验当前状态。 func GetRecommendVideosByIDsContext(ctx context.Context, ids []primitive.ObjectID) ([]*VideoModel, error) { if len(ids) == 0 { return []*VideoModel{}, nil } var out []*VideoModel return out, coll(mdb.ToolCtx(ctx)).Find(&out, recommendVideosByIDsFilter(ids)) } func recommendVideosByIDsFilter(ids []primitive.ObjectID) bson.M { return bson.M{ "_id": bson.M{"$in": ids}, "status": CheckPass, "newsType": SHORT, "deleteAt": nil, "recoWeight": bson.M{"$ne": -1}, } } func DeleteBeforeDeleteAt(t *db.MongoTool, tm time.Time) error { _, err := coll(t).DeleteMany(bson.M{"deletedAt": bson.M{"$lt": tm}}) return err } func GetVideosRecommandInVids(vids []primitive.ObjectID) ([]*VideoModel, error) { var vms []*VideoModel opts := options.Find().SetSort(bson.D{{Key: "effectivePlayCount", Value: -1}}).SetLimit(20) return vms, coll(nil).Find(&vms, bson.M{"_id": bson.M{"$in": vids}, "status": 1, "reviewAt": bson.M{"$gte": time.Now().Add(time.Hour * 24 * -200)}}, opts) } func GetSearchRecommand() (data []*VideoModel, err error) { //加一个搜索缓存 key := fmt.Sprintf("searchRecommend_search") expaire := time.Minute * 2 s, err := appg.Redis.Get(key) if err != nil { return } if s != nil && *s != "" { err = json.Unmarshal([]byte(*s), &data) return data, err } if data == nil { var rechargeVms []*VideoModel var freeVms []*VideoModel opts := options.Find().SetSort(bson.D{{Key: "effectivePlayCount", Value: -1}}).SetLimit(10) tm := time.Now().Add(time.Hour * 24 * -200) var wg sync.WaitGroup wg.Add(2) common.Go(func() { defer wg.Done() if err := coll(nil).Find(&rechargeVms, bson.M{"coins": bson.M{"$gt": 0}, "status": 1, "reviewAt": bson.M{"$gte": tm}}, opts); err != nil { return } }) common.Go(func() { defer wg.Done() if err := coll(nil).Find(&freeVms, bson.M{"coins": 0, "status": 1, "reviewAt": bson.M{"$gte": tm}}, opts); err != nil { return } }) wg.Wait() data = append(data, rechargeVms...) data = append(data, freeVms...) sort.Slice(data, func(i, j int) bool { return data[i].FakePlayCount > data[j].FakePlayCount }) common.Go(func() { b, _ := json.Marshal(data) _ = appg.Redis.Set(key, string(b), expaire) }) } return data, nil } func FindOneByFilter(cond bson.M) (vidInfo *VideoModel, err error) { err = coll(nil).FindOne(&vidInfo, cond) return } // ExportFindMany 导出的时候查询所有 func ExportFindMany(filter bson.M, skip int64, size int64) ([]*VideoModel, error) { sort := bson.D{{Key: "createdAt", Value: 1}} opts := options.FindOptions{} opts.SetSort(sort).SetSkip(skip).SetLimit(size) data := make([]*VideoModel, 0) if err := coll(nil).Find(&data, filter, &opts); err != nil { log.ZapLog.Warn(" ExportFindMany Find fail", log.E(err)) return data, err } return data, nil } func GetShortVideoListByUpdateTimeRange(start time.Time, end time.Time, page int, size int) (data []*VideoModel, hasNext bool, err error) { var query = bson.M{ //"newsType": SHORT, 防止改类型 "playTime": bson.M{"$lte": 300}, "updatedAt": bson.M{"$gte": start, "$lt": end}, } opts := options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetSkip(int64((page - 1) * size)).SetLimit(int64(size)) data, hasNext, err = FindList(query, opts) if err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByUpdateTimeRange", table, "Find", err), log.Any("start", start), log.Any("end", end), ) return } return } //==================================================================================================== //==================================================================================================== //==================================================================================================== //==================================================================================================== //==================================================================================================== type FilterType interface { string | primitive.ObjectID | bson.M } func makeFilter[T FilterType](cond T) (bson.M, error) { var ( filter bson.M c any = cond ) switch vidStr := c.(type) { case string: oid, err := primitive.ObjectIDFromHex(vidStr) if err != nil { log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "ObjectIDFromHex", err), log.Any("oid", oid)) return nil, err } filter = bson.M{"_id": oid} case primitive.ObjectID: filter = bson.M{"_id": c} default: // bson.M filter = c.(bson.M) } return filter, nil } // Create 创建数据 func Create(t *db.MongoTool, data ...*VideoModel) error { r, err := coll(t).InsertMany(data) if err != nil { log.Error(fmt.Sprintf("[METHOD-Create]==> Model %s %s fail error:%+v", table, "InsertMany", err), log.Any("data", data)) return err } if len(r.InsertedIDs) == len(data) { for i, d := range r.InsertedIDs { data[i].ID = d.(primitive.ObjectID) } } return nil } // DeleteOne 根据条件删除一条 func DeleteOne[T FilterType](cond T, opt ...*options.DeleteOptions) error { filter, err := makeFilter(cond) if err != nil { return err } _, err = coll(nil).DeleteOne(filter, opt...) if err != nil { log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "DeleteOne", err), log.Any("filter", filter)) } return err } // DeleteMany 根据条件删除一条或者多条 func DeleteMany[T FilterType](cond T, opt ...*options.DeleteOptions) error { filter, err := makeFilter(cond) if err != nil { return err } _, err = coll(nil).DeleteMany(filter, opt...) if err != nil { log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "DeleteMany", err), log.Any("filter", filter)) } return err } // FindList 条件获取列表 func FindList(filter bson.M, opts *options.FindOptions, count ...*int64) (out []*VideoModel, hasNext bool, err error) { if opts == nil { opts = options.Find() } if opts.Limit == nil { opts.SetLimit(1000) } if opts.Sort == nil { opts.SetSort(bson.D{{Key: "_id", Value: -1}}) } // 不需要统计总条数 就不要创建count,避免无用的查询 if len(count) == 1 { *count[0], err = coll(nil).Count(filter) if err != nil { return nil, false, err } } limit := int(*opts.Limit) opts.SetLimit(int64(limit + 1)) err = coll(nil).Find(&out, filter, opts) if err != nil { log.Error(fmt.Sprintf("[METHOD-FetchList]==> Model %s Find fail error:%+v:", table, err), log.Any("filter", filter)) return out, false, err } hasNext = len(out) > limit if hasNext { out = out[:limit] } return out, hasNext, nil } // FindOne 通过id获取详细信息 func FindOne[T FilterType](cond T, opt ...*options.FindOneOptions) (v *VideoModel, err error) { filter, err := makeFilter(cond) if err != nil { return nil, err } err = coll(nil).FindOne(&v, filter, opt...) if err != nil { log.Error(fmt.Sprintf("[METHOD-FetchInfoById]==> Model %s %s fail error:%+v:", table, "FindOne", err), log.Any("filter", filter)) return v, err } return v, nil } // UpdateOne 单条更新 func UpdateOne[T FilterType](cond T, data bson.M) (err error) { filter, err := makeFilter(cond) if err != nil { return err } _, err = coll(nil).UpdateOne(filter, data) if err != nil { log.Error(fmt.Sprintf("[METHOD-Update]==> Model %s %s fail error:%+v:", table, "UpdateOne", err), log.Any("filter", filter)) return err } return nil } // UpdateMany 批量更新 func UpdateMany[T FilterType](cond T, data bson.M) (err error) { filter, err := makeFilter(cond) if err != nil { return err } _, err = coll(nil).UpdateMany(filter, data) if err != nil { log.Error(fmt.Sprintf("[METHOD-Update]==> Model %s %s fail error:%+v:", table, "UpdateOne", err), log.Any("filter", filter)) return err } return nil } // GetVideoListByIDsPublish 通过视频id获取状态为审核通过的视频 func GetVideoListByIDsPublish(vIds []ObjectID) (back []*VideoModel, err error) { opts := options.Find().SetSort(bson.D{{Key: "reviewAt", Value: -1}, {Key: "createdAt", Value: -1}}) cond := M{"_id": bson.M{"$in": vIds}, "status": CheckPass} if err = coll(nil).Find(&back, cond, opts); err != nil { log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByIDsPublish", table, "Find", err)) } return } // FindForReviewBatch 按 _id 游标分页扫描上架视频,用于内容审查任务 // 仅扫描 status ∈ {CheckPass(1), Free(3)}(与"已上架"语义一致) // lastID 传 primitive.NilObjectID 表示从头开始;limit 控制每批数量 func FindForReviewBatch(lastID primitive.ObjectID, limit int64) ([]*VideoModel, error) { cond := bson.M{"status": bson.M{"$in": []int{CheckPass, Free}}} if !lastID.IsZero() { cond["_id"] = bson.M{"$gt": lastID} } opts := options.Find(). SetSort(bson.D{{Key: "_id", Value: 1}}). SetLimit(limit) var list []*VideoModel if err := coll(nil).Find(&list, cond, opts); err != nil { return nil, err } return list, nil } // CountForReview 上架视频总数(用于任务初始化);条件与 FindForReviewBatch 保持一致 func CountForReview() (int64, error) { return coll(nil).Count(bson.M{"status": bson.M{"$in": []int{CheckPass, Free}}}) } // UpdateForReview 内容审查通过后回写文本字段 // title/content/richText 任一非空则更新对应字段 func UpdateForReview(id primitive.ObjectID, title, content, richText string) error { set := bson.M{} if title != "" { set["title"] = title } if content != "" { set["content"] = content } if richText != "" { set["richText"] = richText } if len(set) == 0 { return nil } set["updatedAt"] = time.Now() _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) return err } // OffShelfManyForReview 内容审查命中后批量下架视频(status -> OffShelf) // 仅当前状态为 CheckPass/Free 的会被改动,已删除/已下架等状态保持不变 func OffShelfManyForReview(ids []primitive.ObjectID) error { if len(ids) == 0 { return nil } _, err := coll(nil).UpdateMany( bson.M{"_id": bson.M{"$in": ids}, "status": bson.M{"$in": []int{CheckPass, Free}}}, bson.M{"$set": bson.M{"status": OffShelf, "updatedAt": time.Now()}}, ) return err }