Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

3521 lines
108 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}