package srv_playlet import ( "context" "math/rand" "regexp" "strings" "time" "videoweb/pub/common/enum/redisconst" "videoweb/pub/common/log" "videoweb/pub/common/playletcache" "videoweb/pub/common/response" "videoweb/pub/common/slugx" "videoweb/pub/common/srvdef" "videoweb/pub/common/utils" "videoweb/pub/models" "videoweb/pub/models/commentlikemod" "videoweb/pub/models/commentmod" "videoweb/pub/models/contenttagmod" "videoweb/pub/models/mwacc" "videoweb/pub/models/playletcategorymod" "videoweb/pub/models/playletchaptermod" "videoweb/pub/models/playletfavoritemod" "videoweb/pub/models/playletlikemod" "videoweb/pub/models/playletmod" "videoweb/pub/models/playletthememod" "videoweb/pub/mongos" "videoweb/pub/pubdef" "videoweb/pub/redis" "videoweb/web/api/proto" "videoweb/web/service/srv_contenttag" "videoweb/web/service/srv_mediaresource" "videoweb/web/service/srv_search" "videoweb/web/service/srv_seopush" "videoweb/web/skd/playletwatermark" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var playletWebProjection = bson.D{ {Key: playletmod.FN.Playlet.Id, Value: 1}, {Key: playletmod.FN.Playlet.Status, Value: 1}, {Key: playletmod.FN.Playlet.IsFeatured, Value: 1}, {Key: playletmod.FN.Playlet.IsOriginal, Value: 1}, {Key: playletmod.FN.Playlet.ShowOriginal, Value: 1}, {Key: playletmod.FN.Playlet.IsRecommend, Value: 1}, {Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1}, {Key: playletmod.FN.Playlet.Banner, Value: 1}, {Key: playletmod.FN.Playlet.ReviewAt, Value: 1}, {Key: playletmod.FN.Playlet.OnlineAt, Value: 1}, {Key: playletmod.FN.Playlet.Title, Value: 1}, {Key: playletmod.FN.Playlet.Slug, Value: 1}, {Key: playletmod.FN.Playlet.Desc, Value: 1}, {Key: playletmod.FN.Playlet.Anchor, Value: 1}, {Key: playletmod.FN.Playlet.Cover, Value: 1}, {Key: playletmod.FN.Playlet.CompressedCover, Value: 1}, {Key: playletmod.FN.Playlet.Preview, Value: 1}, {Key: playletmod.FN.Playlet.CategoryIds, Value: 1}, {Key: playletmod.FN.Playlet.CategoryBinds, Value: 1}, {Key: playletmod.FN.Playlet.ThemeIds, Value: 1}, {Key: playletmod.FN.Playlet.TagIds, Value: 1}, {Key: playletmod.FN.Playlet.ContentType, Value: 1}, {Key: playletmod.FN.Playlet.PayType, Value: 1}, {Key: playletmod.FN.Playlet.SingleCoins, Value: 1}, {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1}, {Key: playletmod.FN.Playlet.FreeEpisodeCount, Value: 1}, {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1}, {Key: playletmod.FN.Playlet.TopRank, Value: 1}, {Key: playletmod.FN.Playlet.RecommendRank, Value: 1}, {Key: playletmod.FN.Playlet.CountComment, Value: 1}, {Key: playletmod.FN.Playlet.CountBuy, Value: 1}, {Key: playletmod.FN.Playlet.CountBrowse, Value: 1}, {Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1}, {Key: playletmod.FN.Playlet.HeatValue, Value: 1}, {Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1}, {Key: playletmod.FN.Playlet.CountCollect, Value: 1}, {Key: playletmod.FN.Playlet.CountLike, Value: 1}, {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1}, {Key: playletmod.FN.Playlet.CountDisLike, Value: 1}, {Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1}, {Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1}, {Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1}, {Key: playletmod.FN.Playlet.OperatorId, Value: 1}, {Key: playletmod.FN.Playlet.OperatorName, Value: 1}, {Key: playletmod.FN.Playlet.CreatedAt, Value: 1}, {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1}, } // ListPlaylet 分页查询短剧列表 func ListPlaylet(ctx context.Context, req *proto.PlayletListReq) (*proto.PlayletListPageResp, *srvdef.SrvErr) { if req == nil { return nil, srvdef.New(srvdef.ErrParamError, "", "") } filter := buildPlayletListFilter(req) pageNum, pageSize := normalizePage(req.PageNum, req.PageSize) totalCount, err := playletmod.CountPlaylet(ctx, filter) if err != nil { return nil, err } resp := &proto.PlayletListPageResp{ BasePageResp: response.BasePageResp{ TotalCount: totalCount, PageSize: int(pageSize), PageNum: int(pageNum), List: make([]*proto.PlayletListResp, 0), }, } if totalCount == 0 { return resp, nil } var list playletmod.PlayletList if req.CategoryId != nil && *req.CategoryId > 0 { list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, *req.CategoryId, (pageNum-1)*pageSize, pageSize, playletmod.CategoryRankAdminSort(), playletWebProjection) } else { list, err = playletmod.FindManyPlaylet(ctx, filter, options.Find(). SetProjection(playletWebProjection). SetSort(bson.D{ {Key: playletmod.FN.Playlet.CreatedAt, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}, }). SetSkip((pageNum-1)*pageSize). SetLimit(pageSize)) } if err != nil { return nil, err } categoryNameMap, err := buildCategoryNameMap(ctx, list) if err != nil { return nil, err } themeNameMap, err := buildThemeNameMap(ctx, list) if err != nil { return nil, err } tagNameMap, err := buildTagNameMap(ctx, list) if err != nil { return nil, err } items := make([]*proto.PlayletListResp, 0, len(list)) for _, item := range list { if item == nil { continue } webItem := playletToWebResp(item, categoryNameMap, themeNameMap, tagNameMap) if req.CategoryId != nil { webItem.CategoryRank = playletmod.CategoryRankOf(playletmod.EffectiveCategoryBinds(item), *req.CategoryId) } items = append(items, webItem) } resp.List = items return resp, nil } // AddPlaylet 新增短剧 func AddPlaylet(ctx context.Context, req *proto.PlayletAddReq, operatorId int64) (*proto.PlayletAddResp, *srvdef.SrvErr) { if req == nil { return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } title := strings.TrimSpace(req.Title) if title == "" { return nil, srvdef.New(srvdef.ErrParamError, "短剧标题不能为空", "") } categoryBinds, err := resolveAddCategoryBinds(req.CategoryBinds, req.CategoryIds) if err != nil { return nil, err } categoryIds := playletmod.CategoryIdsFromBinds(categoryBinds) if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil { return nil, err } themeIds := dedupePositiveInt64Ids(req.ThemeIds) if len(themeIds) == 0 { return nil, srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "") } if err := validatePlayletThemeIds(ctx, themeIds); err != nil { return nil, err } tagIds, err := srv_contenttag.NormalizeContentTagIds(req.TagIds) if err != nil { return nil, err } if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil { return nil, err } if !isValidPlayletStatus(req.Status) { return nil, srvdef.New(srvdef.ErrParamError, "上下架状态无效", "") } if !isValidPlayletContentType(req.ContentType) { return nil, srvdef.New(srvdef.ErrParamError, "内容类型无效", "") } payType, singleCoins, err := normalizePlayletPaySetting(req.PayType, &req.SingleCoins, true) if err != nil { return nil, err } freeEpisodeCount := int64(2) if req.FreeEpisodeCount != nil { if !validOfficialFreeEpisodeCount(*req.FreeEpisodeCount) { return nil, srvdef.New(srvdef.ErrParamError, "官网开放集数无效", "") } freeEpisodeCount = *req.FreeEpisodeCount } banner := strings.TrimSpace(req.Banner) if req.ShowOnBanner && banner == "" { return nil, srvdef.New(srvdef.ErrParamError, "显示首页banner时banner图不能为空", "") } now := time.Now() operatorId, operatorName := resolvePlayletOperator(ctx, operatorId) doc := &playletmod.Playlet{ Status: req.Status, IsFeatured: req.IsFeatured, IsOriginal: true, // 管理后台新增的短剧都是原创 ShowOriginal: req.ShowOriginal, IsRecommend: req.IsRecommend, ShowOnBanner: req.ShowOnBanner, Banner: banner, ReviewAt: now, OnlineAt: playletOnlineAt(req.Status, now), CategoryIds: categoryIds, CategoryBinds: categoryBinds, ThemeIds: themeIds, Title: title, Slug: req.Slug, Desc: strings.TrimSpace(req.Desc), Anchor: strings.TrimSpace(req.Anchor), Cover: strings.TrimSpace(req.Cover), Preview: strings.TrimSpace(req.Preview), TagIds: tagIds, ContentType: req.ContentType, PayType: payType, SingleCoins: singleCoins, TotalEpisode: req.TotalEpisode, FreeEpisodeCount: freeEpisodeCount, TopRank: req.TopRank, RecommendRank: req.RecommendRank, SeoInfo: normalizeSeoValue(req.SeoInfo), OperatorId: operatorId, OperatorName: operatorName, } exists := func(s string) bool { item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, 0)) return item != nil } if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, title), exists); serr != nil { return nil, serr } if err := playletmod.InsertOnePlaylet(ctx, doc); err != nil { return nil, err } if err := srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil { return nil, err } go srv_mediaresource.FillPlayletCompressedCover(context.Background(), doc) clearAppPlayletHomeCache() if req.ShowOriginal { playletcache.ClearRankCache() } srv_seopush.NotifyPlayletAfterWrite(ctx, doc.Id, nil, doc.Status, doc.OnlineAt) if doc.Status == playletmod.StatusUp { asyncSyncPlayletESByStatus([]int64{doc.Id}, doc.Status, "短剧新增") } return &proto.PlayletAddResp{Id: doc.Id}, nil } // UpdatePlaylet 更新短剧 func UpdatePlaylet(ctx context.Context, req *proto.PlayletUpdateReq, operatorId int64) *srvdef.SrvErr { if req == nil || req.Id <= 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID无效", "") } current, err := playletmod.FindOnePlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: req.Id, }) if err != nil { return err } now := time.Now() setDoc := bson.M{playletmod.FN.Playlet.UpdatedAt: now} if req.Status != nil { if !isValidPlayletStatus(*req.Status) { return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "") } setDoc[playletmod.FN.Playlet.Status] = *req.Status setDoc[playletmod.FN.Playlet.ReviewAt] = now if *req.Status == playletmod.StatusUp { setDoc[playletmod.FN.Playlet.OnlineAt] = now setDoc[playletmod.FN.Playlet.IndexNow] = int64(0) } } if req.FreeEpisodeCount != nil { if !validOfficialFreeEpisodeCount(*req.FreeEpisodeCount) { return srvdef.New(srvdef.ErrParamError, "官网开放集数无效", "") } setDoc[playletmod.FN.Playlet.FreeEpisodeCount] = *req.FreeEpisodeCount } if req.IsFeatured != nil { setDoc[playletmod.FN.Playlet.IsFeatured] = *req.IsFeatured } if req.ShowOriginal != nil { setDoc[playletmod.FN.Playlet.ShowOriginal] = *req.ShowOriginal } if req.IsRecommend != nil { setDoc[playletmod.FN.Playlet.IsRecommend] = *req.IsRecommend } if req.ShowOnBanner != nil { setDoc[playletmod.FN.Playlet.ShowOnBanner] = *req.ShowOnBanner } if req.Banner != nil { setDoc[playletmod.FN.Playlet.Banner] = strings.TrimSpace(*req.Banner) } if req.CategoryBinds != nil || req.CategoryIds != nil { categoryBinds, err := resolveCategoryBinds(req.CategoryBinds, req.CategoryIds, playletmod.EffectiveCategoryBinds(current)) if err != nil { return err } categoryIds := playletmod.CategoryIdsFromBinds(categoryBinds) if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil { return err } setDoc[playletmod.FN.Playlet.CategoryIds] = categoryIds setDoc[playletmod.FN.Playlet.CategoryBinds] = categoryBinds } if req.ThemeIds != nil { themeIds := dedupePositiveInt64Ids(*req.ThemeIds) if len(themeIds) == 0 { return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "") } if err := validatePlayletThemeIds(ctx, themeIds); err != nil { return err } setDoc[playletmod.FN.Playlet.ThemeIds] = themeIds } if req.Title != nil { title := strings.TrimSpace(*req.Title) if title == "" { return srvdef.New(srvdef.ErrParamError, "短剧标题不能为空", "") } setDoc[playletmod.FN.Playlet.Title] = title } if req.Desc != nil { setDoc[playletmod.FN.Playlet.Desc] = strings.TrimSpace(*req.Desc) } if req.Anchor != nil { setDoc[playletmod.FN.Playlet.Anchor] = strings.TrimSpace(*req.Anchor) } coverChanged := false if req.Cover != nil { newCover := strings.TrimSpace(*req.Cover) setDoc[playletmod.FN.Playlet.Cover] = newCover if strings.TrimSpace(current.Cover) != newCover { setDoc[playletmod.FN.Playlet.CompressedCover] = "" coverChanged = true } } if req.Preview != nil { setDoc[playletmod.FN.Playlet.Preview] = strings.TrimSpace(*req.Preview) } var oldTagIds []int64 var newTagIds []int64 if req.TagIds != nil { tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds) if err != nil { return err } if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil { return err } oldTagIds = append([]int64{}, current.TagIds...) newTagIds = tagIds setDoc[playletmod.FN.Playlet.TagIds] = tagIds } if req.ContentType != nil { if !isValidPlayletContentType(*req.ContentType) { return srvdef.New(srvdef.ErrParamError, "内容类型无效", "") } setDoc[playletmod.FN.Playlet.ContentType] = *req.ContentType } if req.PayType != nil { payType, singleCoins, err := normalizePlayletPaySetting(*req.PayType, req.SingleCoins, true) if err != nil { return err } setDoc[playletmod.FN.Playlet.PayType] = payType setDoc[playletmod.FN.Playlet.SingleCoins] = singleCoins } else if req.SingleCoins != nil { if *req.SingleCoins < 0 { return srvdef.New(srvdef.ErrParamError, "单集金币不能小于0", "") } setDoc[playletmod.FN.Playlet.SingleCoins] = *req.SingleCoins } if req.TotalEpisode != nil { setDoc[playletmod.FN.Playlet.TotalEpisode] = *req.TotalEpisode } if req.TopRank != nil { setDoc[playletmod.FN.Playlet.TopRank] = *req.TopRank } if req.RecommendRank != nil { setDoc[playletmod.FN.Playlet.RecommendRank] = *req.RecommendRank } if req.SeoInfo != nil { setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo) } title := current.Title if req.Title != nil { title = strings.TrimSpace(*req.Title) } keywords := current.SeoInfo.Keyworld if req.SeoInfo != nil { keywords = normalizeSeoValue(*req.SeoInfo).Keyworld } exists := func(s string) bool { item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, req.Id)) return item != nil } slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug, utils.SlugSourceFromKeywordsOrTitle(keywords, title), exists) if serr != nil { return serr } if changed { setDoc[playletmod.FN.Playlet.Slug] = slug } if len(setDoc) <= 1 { return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "") } operatorId, operatorName := resolvePlayletOperator(ctx, operatorId) setDoc[playletmod.FN.Playlet.OperatorId] = operatorId setDoc[playletmod.FN.Playlet.OperatorName] = operatorName if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{"$set": setDoc}, ); err != nil { return err } if req.TagIds != nil { if err := srv_contenttag.SyncContentTagBindCount(ctx, oldTagIds, newTagIds, contenttagmod.ContentTagTypePlaylet); err != nil { return err } } if coverChanged { go func(playletId int64, cover string) { srv_mediaresource.FillPlayletCompressedCover(context.Background(), &playletmod.Playlet{ Id: playletId, Cover: cover, }) }(req.Id, strings.TrimSpace(*req.Cover)) } clearAppPlayletHomeCache() if req.ShowOriginal != nil { playletcache.ClearRankCache() } status := current.Status onlineAt := current.OnlineAt if req.Status != nil { status = *req.Status if status == playletmod.StatusUp { onlineAt = now } } prevStatus := current.Status srv_seopush.NotifyPlayletAfterWrite(ctx, req.Id, &prevStatus, status, onlineAt) asyncSyncPlayletESByLatestStatus([]int64{req.Id}, "短剧更新") return nil } // DelPlaylet 删除短剧、剧集及关联的用户数据 func DelPlaylet(ctx context.Context, req *proto.PlayletDelReq) *srvdef.SrvErr { if req == nil || len(req.Ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } for _, id := range req.Ids { if id <= 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID无效", "") } } ids := dedupePositiveInt64Ids(req.Ids) playletFilter := bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}} chapterFilter := bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": ids}} var serviceErr *srvdef.SrvErr var chapterIDs []int64 txnErr := mongos.Shop.Transaction(ctx, func(sctx mongo.SessionContext) error { serviceErr = nil playlets, err := playletmod.FindManyPlaylet(sctx, playletFilter, options.Find().SetProjection(bson.M{ playletmod.FN.Playlet.Id: 1, playletmod.FN.Playlet.TagIds: 1, })) if err != nil { serviceErr = err return err } changes := make([]srv_contenttag.ContentTagBindChange, 0, len(playlets)) for _, playlet := range playlets { if playlet != nil && len(playlet.TagIds) > 0 { changes = append(changes, srv_contenttag.ContentTagBindChange{OldTagIds: playlet.TagIds}) } } if len(changes) > 0 { if err := srv_contenttag.SyncContentTagBindCountBatch(sctx, changes, contenttagmod.ContentTagTypePlaylet); err != nil { serviceErr = err return err } } chapters, err := playletchaptermod.FindManyPlayletChapter(sctx, chapterFilter, options.Find().SetProjection(bson.M{ playletchaptermod.FN.PlayletChapter.Id: 1, })) if err != nil { serviceErr = err return err } commentFilters := bson.A{bson.M{ commentmod.FN.Comment.ObjectType: pubdef.CommentObjectTypePlaylet, commentmod.FN.Comment.ObjectId: bson.M{"$in": ids}, }} chapterIDs = make([]int64, 0, len(chapters)) for _, chapter := range chapters { if chapter != nil { chapterIDs = append(chapterIDs, chapter.Id) } } if len(chapterIDs) > 0 { commentFilters = append(commentFilters, bson.M{ commentmod.FN.Comment.ObjectType: pubdef.CommentObjectTypePlayletChapter, commentmod.FN.Comment.ObjectId: bson.M{"$in": chapterIDs}, }) } commentFilter := bson.M{"$or": commentFilters} comments, err := commentmod.FindManyComment(sctx, commentFilter, options.Find().SetProjection(bson.M{ commentmod.FN.Comment.Id: 1, })) if err != nil { serviceErr = err return err } commentIDs := make([]int64, 0, len(comments)) for _, comment := range comments { if comment != nil { commentIDs = append(commentIDs, comment.Id) } } if err := commentlikemod.DeleteByCommentIds(sctx, commentIDs); err != nil { serviceErr = err return err } deletions := []struct { collection string filter bson.M }{ {commentmod.TN.Comment, commentFilter}, {playletlikemod.TN.PlayletLike, bson.M{playletlikemod.FN.PlayletLike.PlayletID: bson.M{"$in": ids}}}, {playletfavoritemod.CollectionName, bson.M{playletfavoritemod.FieldPlayletID: bson.M{"$in": ids}}}, {models.WatchHistoryCollectionName, bson.M{models.WatchHistoryDataType: models.WatchHistoryTypePlaylet, models.WatchHistoryContentID: bson.M{"$in": ids}}}, {playletchaptermod.TN.PlayletChapter, chapterFilter}, {playletmod.TN.Playlet, playletFilter}, } for _, deletion := range deletions { if _, err := mongos.Shop.Coll(deletion.collection).DeleteMany(sctx, deletion.filter); err != nil { log.Warn("删除短剧关联数据失败", log.Any("collection", deletion.collection), log.E(err)) serviceErr = srvdef.New(srvdef.ErrDbDeleteError, "", "") return err } } return nil }) if txnErr != nil { if serviceErr != nil { return serviceErr } log.Warn("删除短剧事务失败", log.E(txnErr)) return srvdef.New(srvdef.ErrDbDeleteError, "", "") } clearAppPlayletHomeCache() playletcache.ClearRankCache() if redis.Client() != nil { for _, id := range ids { if err := redis.Del(redisconst.CommentListCacheKey(id, pubdef.CommentObjectTypePlaylet)); err != nil { log.Warn("清理短剧评论缓存失败", log.Any("playletId", id), log.E(err)) } } for _, id := range chapterIDs { if err := redis.Del(redisconst.CommentListCacheKey(id, pubdef.CommentObjectTypePlayletChapter)); err != nil { log.Warn("清理剧集评论缓存失败", log.Any("chapterId", id), log.E(err)) } } } srv_seopush.MarkSitemapDirty(ctx) for _, id := range ids { srv_seopush.EnqueueDeleted(ctx, srv_seopush.KindPlaylet, id) } if err := srv_search.SyncPlayletStatusToES(ctx, ids, playletmod.StatusDown); err != nil { return err } return nil } // SetPlayletStatus 单/批量设置短剧上下架 func SetPlayletStatus(ctx context.Context, req *proto.PlayletStatusSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } if !isValidPlayletStatus(req.Status) { return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "") } now := time.Now() setDoc := bson.M{ playletmod.FN.Playlet.Status: req.Status, playletmod.FN.Playlet.ReviewAt: now, playletmod.FN.Playlet.UpdatedAt: now, } if req.Status == playletmod.StatusUp { setDoc[playletmod.FN.Playlet.OnlineAt] = now setDoc[playletmod.FN.Playlet.IndexNow] = int64(0) } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() srv_seopush.NotifyPlayletAfterStatus(ctx, ids, req.Status, now) asyncSyncPlayletESByStatus(ids, req.Status, "短剧上下架") return nil } // SetPlayletRecommend 单/批量设置短剧是否推荐 func SetPlayletRecommend(ctx context.Context, req *proto.PlayletRecommendSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } setDoc := bson.M{ playletmod.FN.Playlet.IsRecommend: req.IsRecommend, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧推荐状态变更") return nil } // SetPlayletFeatured 单/批量设置短剧是否爆剧 func SetPlayletFeatured(ctx context.Context, req *proto.PlayletFeaturedSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } setDoc := bson.M{ playletmod.FN.Playlet.IsFeatured: req.IsFeatured, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧爆剧状态变更") return nil } // SetPlayletBanner 单/批量设置短剧是否显示在首页banner func SetPlayletBanner(ctx context.Context, req *proto.PlayletBannerSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } setDoc := bson.M{ playletmod.FN.Playlet.ShowOnBanner: req.ShowOnBanner, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧banner状态变更") return nil } // SetPlayletTag 单/批量设置短剧标签 func SetPlayletTag(ctx context.Context, req *proto.PlayletTagSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } if req.TagIds == nil { return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "") } tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds) if err != nil { return err } if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil { return err } list, err := playletmod.FindManyPlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: bson.M{"$in": ids}, }, options.Find().SetProjection(bson.D{ {Key: playletmod.FN.Playlet.Id, Value: 1}, {Key: playletmod.FN.Playlet.TagIds, Value: 1}, })) if err != nil { return err } setDoc := bson.M{ playletmod.FN.Playlet.TagIds: tagIds, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list)) for _, item := range list { if item == nil { continue } changes = append(changes, srv_contenttag.ContentTagBindChange{ OldTagIds: append([]int64{}, item.TagIds...), NewTagIds: append([]int64{}, tagIds...), }) } if err := srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypePlaylet); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧标签变更") return nil } // SetPlayletCategory 单/批量设置短剧分类 func SetPlayletCategory(ctx context.Context, req *proto.PlayletCategorySetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } if req.CategoryIds == nil { return srvdef.New(srvdef.ErrParamError, "分类ID不能为空", "") } categoryIds, err := normalizePlayletCategoryIds(*req.CategoryIds) if err != nil { return err } if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil { return err } list, err := playletmod.FindManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}) if err != nil { return err } for _, item := range list { binds := playletmod.MergeCategoryBindsByIds(playletmod.EffectiveCategoryBinds(item), categoryIds) if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: item.Id}, bson.M{"$set": bson.M{ playletmod.FN.Playlet.CategoryIds: categoryIds, playletmod.FN.Playlet.CategoryBinds: binds, playletmod.FN.Playlet.UpdatedAt: time.Now(), }}); err != nil { return err } } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧分类变更") return nil } // SetPlayletTheme 单/批量设置短剧题材 func SetPlayletTheme(ctx context.Context, req *proto.PlayletThemeSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } themeIds := dedupePositiveInt64Ids(req.ThemeIds) if len(themeIds) == 0 { return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "") } if err := validatePlayletThemeIds(ctx, themeIds); err != nil { return err } setDoc := bson.M{ playletmod.FN.Playlet.ThemeIds: themeIds, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧题材变更") return nil } // SetPlayletVirtualLike 单/批量设置短剧虚拟点赞数 func SetPlayletVirtualLike(ctx context.Context, req *proto.PlayletVirtualLikeSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } if req.MinVirtualCountLike < 0 || req.MaxVirtualCountLike < req.MinVirtualCountLike { return srvdef.New(srvdef.ErrParamError, "虚拟点赞数范围无效", "") } if req.MaxVirtualCountLike-req.MinVirtualCountLike < int64(len(ids))-1 { return srvdef.New(srvdef.ErrParamError, "虚拟点赞数范围不足,无法保证每条短剧数值不同", "") } values := randomUniqueVirtualLikeValues(req.MinVirtualCountLike, req.MaxVirtualCountLike, len(ids)) now := time.Now() docs := make([]mongo.WriteModel, 0, len(ids)) for index, id := range ids { setDoc := bson.M{ playletmod.FN.Playlet.VirtualCountLike: values[index], playletmod.FN.Playlet.UpdatedAt: now, } docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{playletmod.FN.Playlet.Id: id}). SetUpdate(bson.M{"$set": setDoc})) } if err := playletmod.BulkWritePlaylet(ctx, docs); err != nil { return err } clearAppPlayletHomeCache() asyncSyncPlayletESByLatestStatus(ids, "短剧虚拟点赞数变更") return nil } // SetPlayletVirtualBrowse 单/批量设置短剧虚拟观看数 func SetPlayletVirtualBrowse(ctx context.Context, req *proto.PlayletVirtualBrowseSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } return setPlayletVirtualMetric(ctx, req.Ids, req.MinVirtualCountBrowse, req.MaxVirtualCountBrowse, playletmod.FN.Playlet.VirtualCountBrowse, "短剧虚拟观看数变更") } // SetPlayletVirtualHeat 单/批量设置短剧虚拟热度 func SetPlayletVirtualHeat(ctx context.Context, req *proto.PlayletVirtualHeatSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } return setPlayletVirtualMetric(ctx, req.Ids, req.MinVirtualHeatValue, req.MaxVirtualHeatValue, playletmod.FN.Playlet.VirtualHeatValue, "短剧虚拟热度变更") } // setPlayletVirtualMetric 按区间为每部短剧设置不同虚拟数值 func setPlayletVirtualMetric(ctx context.Context, rawIds []int64, min, max int64, field, reason string) *srvdef.SrvErr { ids := dedupePositiveInt64Ids(rawIds) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } if min < 0 || max < min || max-min < int64(len(ids))-1 { return srvdef.New(srvdef.ErrParamError, "虚拟数值区间无效或不足", "") } values := randomUniqueVirtualLikeValues(min, max, len(ids)) now := time.Now() docs := make([]mongo.WriteModel, 0, len(ids)) for index, id := range ids { docs = append(docs, mongo.NewUpdateOneModel(). SetFilter(bson.M{playletmod.FN.Playlet.Id: id}). SetUpdate(bson.M{"$set": bson.M{field: values[index], playletmod.FN.Playlet.UpdatedAt: now}})) } if err := playletmod.BulkWritePlaylet(ctx, docs); err != nil { return err } clearAppPlayletHomeCache() playletcache.ClearRankCache() asyncSyncPlayletESByLatestStatus(ids, reason) return nil } // SetPlayletPayType 单/批量设置短剧默认支付方式 func SetPlayletPayType(ctx context.Context, req *proto.PlayletPayTypeSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } payType, singleCoins, err := normalizePlayletPaySetting(req.PayType, req.SingleCoins, true) if err != nil { return err } setDoc := bson.M{ playletmod.FN.Playlet.PayType: payType, playletmod.FN.Playlet.SingleCoins: singleCoins, playletmod.FN.Playlet.UpdatedAt: time.Now(), } if err := playletmod.UpdateManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ); err != nil { return err } clearAppPlayletHomeCache() return nil } // ResetPlayletWatermark 按短剧ID批量重置其下全部剧集水印,等待定时任务重新加水印。 // 已有水印地址不清理,新水印完成回写时直接覆盖。 // 原创短剧保留 mediaSourceId / videoUrl / mediaTaskId,直接进入已登记后按原片重跑水印。 // 非原创清空 media 关联并回到未处理,由 SyncMediaFile 重新登记。 func ResetPlayletWatermark(ctx context.Context, req *proto.PlayletWatermarkResetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "") } originalIds, otherIds, err := splitPlayletIdsByOriginal(ctx, ids) if err != nil { return err } now := time.Now() if resetErr := resetOriginalChaptersWatermark(ctx, originalIds, now); resetErr != nil { return resetErr } return resetChaptersWatermark(ctx, otherIds, playletchaptermod.WatermarkStatusPending, now) } func splitPlayletIdsByOriginal(ctx context.Context, ids []int64) (originalIds, otherIds []int64, err *srvdef.SrvErr) { originalList, err := playletmod.FindManyPlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: bson.M{"$in": ids}, playletmod.FN.Playlet.IsOriginal: true, }, options.Find().SetProjection(bson.M{playletmod.FN.Playlet.Id: 1})) if err != nil { return nil, nil, err } originalSet := make(map[int64]struct{}, len(originalList)) for _, item := range originalList { if item == nil || item.Id <= 0 { continue } originalSet[item.Id] = struct{}{} originalIds = append(originalIds, item.Id) } for _, id := range ids { if _, ok := originalSet[id]; !ok { otherIds = append(otherIds, id) } } return originalIds, otherIds, nil } func resetOriginalChaptersWatermark(ctx context.Context, playletIds []int64, now time.Time) *srvdef.SrvErr { if len(playletIds) == 0 { return nil } // 原创剧集保留 mediaSourceId / videoUrl / mediaTaskId,以及已有水印地址。 // 新水印完成回写时会直接覆盖 WatermarkVideoUrl。 err := playletchaptermod.UpdateManyPlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": playletIds}}, bson.M{"$set": bson.M{ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusRegistered, playletchaptermod.FN.PlayletChapter.UpdatedAt: now, }}, ) if err != nil && err.Code == srvdef.ErrNoData { return nil } return err } func resetChaptersWatermark(ctx context.Context, playletIds []int64, status playletchaptermod.WatermarkStatus, now time.Time) *srvdef.SrvErr { if len(playletIds) == 0 { return nil } err := playletchaptermod.UpdateManyPlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": playletIds}}, bson.M{"$set": bson.M{ playletchaptermod.FN.PlayletChapter.WatermarkStatus: status, playletchaptermod.FN.PlayletChapter.MediaSourceId: "", playletchaptermod.FN.PlayletChapter.MediaTaskId: "", playletchaptermod.FN.PlayletChapter.UpdatedAt: now, }}, ) if err != nil && err.Code == srvdef.ErrNoData { return nil } return err } // ListPlayletChapter 分页查询短剧剧集列表 func ListPlayletChapter(ctx context.Context, req *proto.PlayletChapterListReq) (*proto.PlayletChapterListPageResp, *srvdef.SrvErr) { if req == nil { return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } filter := bson.M{} if req.Id != nil && *req.Id > 0 { filter[playletchaptermod.FN.PlayletChapter.Id] = *req.Id } if req.PlayletId != nil && *req.PlayletId > 0 { filter[playletchaptermod.FN.PlayletChapter.PlayletId] = *req.PlayletId } if req.WatermarkStatus != nil { filter[playletchaptermod.FN.PlayletChapter.WatermarkStatus] = *req.WatermarkStatus } pageNum, pageSize := normalizePage(req.PageNum, req.PageSize) totalCount, err := playletchaptermod.CountPlayletChapter(ctx, filter) if err != nil { return nil, err } resp := &proto.PlayletChapterListPageResp{ BasePageResp: response.BasePageResp{ TotalCount: totalCount, PageSize: int(pageSize), PageNum: int(pageNum), List: make([]*proto.PlayletChapterListResp, 0), }, } if totalCount == 0 { return resp, nil } list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find(). SetSort(bson.D{ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1}, {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1}, }). SetSkip((pageNum-1)*pageSize). SetLimit(pageSize)) if err != nil { return nil, err } items := make([]*proto.PlayletChapterListResp, 0, len(list)) for _, item := range list { if item == nil { continue } items = append(items, chapterToWebResp(item)) } resp.List = items return resp, nil } // AddPlayletChapter 新增短剧剧集 func AddPlayletChapter(ctx context.Context, req *proto.PlayletChapterAddReq) (*proto.PlayletChapterAddResp, *srvdef.SrvErr) { if req == nil || req.PlayletId <= 0 || req.CurrentEpisode <= 0 { return nil, srvdef.New(srvdef.ErrParamError, "playletId/currentEpisode无效", "") } if !isValidPlayletChapterPayType(req.PayType) { return nil, srvdef.New(srvdef.ErrParamError, "支付方式无效", "") } if req.PayType == playletchaptermod.PayTypeCoin && req.SingleCoins <= 0 { return nil, srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "") } singleCoins := req.SingleCoins if req.PayType != playletchaptermod.PayTypeCoin { singleCoins = 0 } playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.PlayletId}) if err != nil { return nil, err } if playlet == nil || !playlet.IsOriginal { return nil, srvdef.New(srvdef.ErrParamError, "只有原创短剧支持新增剧集", "") } doc := &playletchaptermod.PlayletChapter{ PlayletId: req.PlayletId, CurrentEpisode: req.CurrentEpisode, Title: strings.TrimSpace(req.Title), Md5: strings.TrimSpace(req.Md5), Hash: strings.TrimSpace(req.Hash), Height: req.Height, Width: req.Width, Ratio: req.Ratio, VideoUrl: strings.TrimSpace(req.VideoUrl), MediaSize: req.MediaSize, MediaTime: req.MediaTime, PayType: req.PayType, SingleCoins: singleCoins, } mediaSourceId := strings.TrimSpace(req.MediaSourceId) if playlet.IsOriginal && (doc.VideoUrl != "" || mediaSourceId != "") { doc.WatermarkStatus = playletchaptermod.WatermarkStatusRegistered if mediaSourceId != "" { // 后台上传正片已在 media 创建原片,直接沿用该 ID,避免再 SyncMediaFile 重复处理。 doc.MediaSourceId = mediaSourceId } else { sourceID, regErr := playletwatermark.RegisterChapterSource(doc) if regErr != nil { log.Error("AddPlayletChapter RegisterChapterSource 失败", log.Any("playletId", req.PlayletId), log.Any("videoUrl", doc.VideoUrl), log.E(regErr)) } else { doc.MediaSourceId = sourceID } } } if err := playletchaptermod.InsertOnePlayletChapter(ctx, doc); err != nil { return nil, err } return &proto.PlayletChapterAddResp{Id: doc.Id}, nil } // DelPlayletChapter 单/批量删除原创短剧剧集。 func DelPlayletChapter(ctx context.Context, req *proto.PlayletChapterDelReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "剧集ID不能为空", "") } filter := bson.M{playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids}} chapters, findErr := playletchaptermod.FindManyPlayletChapter(ctx, filter) if findErr != nil { return findErr } if len(chapters) == 0 { return srvdef.New(srvdef.ErrNoData, "", "") } playletIds := make([]int64, 0, len(chapters)) seenPlaylet := make(map[int64]struct{}, len(chapters)) for _, item := range chapters { if item == nil || item.PlayletId <= 0 { continue } if _, ok := seenPlaylet[item.PlayletId]; ok { continue } seenPlaylet[item.PlayletId] = struct{}{} playletIds = append(playletIds, item.PlayletId) } if len(playletIds) > 0 { playlets, playletErr := playletmod.FindManyPlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: bson.M{"$in": playletIds}, }, options.Find().SetProjection(bson.M{ playletmod.FN.Playlet.Id: 1, playletmod.FN.Playlet.IsOriginal: 1, })) if playletErr != nil { return playletErr } for _, item := range playlets { if item == nil || !item.IsOriginal { return srvdef.New(srvdef.ErrParamError, "只有原创短剧支持删除剧集", "") } } } count, err := playletchaptermod.DelManyPlayletChapter(ctx, filter) if err != nil { return err } if count == 0 { return srvdef.New(srvdef.ErrNoData, "", "") } return nil } // UpdatePlayletChapter 编辑短剧剧集 func UpdatePlayletChapter(ctx context.Context, req *proto.PlayletChapterUpdateReq) *srvdef.SrvErr { if req == nil || req.Id <= 0 { return srvdef.New(srvdef.ErrParamError, "id无效", "") } setDoc := bson.M{playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now()} if req.CurrentEpisode != nil { if *req.CurrentEpisode <= 0 { return srvdef.New(srvdef.ErrParamError, "集数无效", "") } setDoc[playletchaptermod.FN.PlayletChapter.CurrentEpisode] = *req.CurrentEpisode } if req.Title != nil { setDoc[playletchaptermod.FN.PlayletChapter.Title] = strings.TrimSpace(*req.Title) } if req.Md5 != nil { setDoc[playletchaptermod.FN.PlayletChapter.Md5] = strings.TrimSpace(*req.Md5) } if req.Hash != nil { setDoc[playletchaptermod.FN.PlayletChapter.Hash] = strings.TrimSpace(*req.Hash) } if req.Height != nil { setDoc[playletchaptermod.FN.PlayletChapter.Height] = *req.Height } if req.Width != nil { setDoc[playletchaptermod.FN.PlayletChapter.Width] = *req.Width } if req.Ratio != nil { setDoc[playletchaptermod.FN.PlayletChapter.Ratio] = *req.Ratio } if req.VideoUrl != nil { setDoc[playletchaptermod.FN.PlayletChapter.VideoUrl] = strings.TrimSpace(*req.VideoUrl) } if req.MediaSize != nil { setDoc[playletchaptermod.FN.PlayletChapter.MediaSize] = *req.MediaSize } if req.MediaTime != nil { setDoc[playletchaptermod.FN.PlayletChapter.MediaTime] = *req.MediaTime } if req.PayType != nil { if !isValidPlayletChapterPayType(*req.PayType) { return srvdef.New(srvdef.ErrParamError, "支付方式无效", "") } setDoc[playletchaptermod.FN.PlayletChapter.PayType] = *req.PayType if *req.PayType != playletchaptermod.PayTypeCoin && req.SingleCoins == nil { setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = int64(0) } } if req.SingleCoins != nil { if *req.SingleCoins < 0 { return srvdef.New(srvdef.ErrParamError, "单集金币不能小于0", "") } setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = *req.SingleCoins } if len(setDoc) <= 1 { return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "") } return playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: req.Id}, bson.M{"$set": setDoc}, ) } // SetPlayletChapterPayType 单/批量设置短剧剧集支付方式 func SetPlayletChapterPayType(ctx context.Context, req *proto.PlayletChapterPayTypeSetReq) *srvdef.SrvErr { if req == nil { return srvdef.New(srvdef.ErrParamError, "参数不能为空", "") } ids := dedupePositiveInt64Ids(req.Ids) if len(ids) == 0 { return srvdef.New(srvdef.ErrParamError, "剧集ID不能为空", "") } if !isValidPlayletChapterPayType(req.PayType) { return srvdef.New(srvdef.ErrParamError, "支付方式无效", "") } setDoc := bson.M{ playletchaptermod.FN.PlayletChapter.PayType: req.PayType, playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now(), } if req.PayType == playletchaptermod.PayTypeCoin { if req.SingleCoins == nil || *req.SingleCoins <= 0 { return srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "") } setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = *req.SingleCoins } else { setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = int64(0) } return playletchaptermod.UpdateManyPlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids}}, bson.M{"$set": setDoc}, ) } func buildPlayletListFilter(req *proto.PlayletListReq) bson.M { filter := bson.M{} if req.Id != nil && *req.Id > 0 { filter[playletmod.FN.Playlet.Id] = *req.Id } if req.CategoryId != nil && *req.CategoryId > 0 { filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{*req.CategoryId}} } if req.TagId != nil && *req.TagId > 0 { filter[playletmod.FN.Playlet.TagIds] = bson.M{"$in": []int64{*req.TagId}} } if req.ThemeId != nil && *req.ThemeId > 0 { filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{*req.ThemeId}} } if req.ContentType != nil { filter[playletmod.FN.Playlet.ContentType] = *req.ContentType } if req.PayType != nil { filter[playletmod.FN.Playlet.PayType] = *req.PayType } if req.Status != nil { filter[playletmod.FN.Playlet.Status] = *req.Status } if req.IsFeatured != nil { filter[playletmod.FN.Playlet.IsFeatured] = *req.IsFeatured } if req.IsOriginal != nil { if *req.IsOriginal { filter[playletmod.FN.Playlet.IsOriginal] = true } else { // 历史入库数据可能没有该字段,按非原创处理 filter[playletmod.FN.Playlet.IsOriginal] = bson.M{"$ne": true} } } if req.ShowOriginal != nil { if *req.ShowOriginal { filter[playletmod.FN.Playlet.ShowOriginal] = true } else { filter[playletmod.FN.Playlet.ShowOriginal] = bson.M{"$ne": true} } } if req.IsRecommend != nil { filter[playletmod.FN.Playlet.IsRecommend] = *req.IsRecommend } if req.ShowOnBanner != nil { filter[playletmod.FN.Playlet.ShowOnBanner] = *req.ShowOnBanner } if title := strings.TrimSpace(req.Title); title != "" { filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)} } if operatorName := strings.TrimSpace(req.OperatorName); operatorName != "" { filter[playletmod.FN.Playlet.OperatorName] = bson.M{"$regex": regexp.QuoteMeta(operatorName)} } return filter } func playletToWebResp(item *playletmod.Playlet, categoryNameMap map[int64]string, themeNameMap map[int64]string, tagNameMap map[int64]string) *proto.PlayletListResp { return &proto.PlayletListResp{ Id: item.Id, Status: item.Status, IsFeatured: item.IsFeatured, IsOriginal: item.IsOriginal, ShowOriginal: item.ShowOriginal, MediaBusiness: item.MediaBusiness, IsRecommend: item.IsRecommend, ShowOnBanner: item.ShowOnBanner, Banner: item.Banner, ReviewAt: item.ReviewAt, OnlineAt: item.OnlineAt, Title: item.Title, Slug: item.Slug, Desc: item.Desc, Anchor: item.Anchor, Cover: item.Cover, CompressedCover: item.CompressedCover, Preview: item.Preview, CategoryIds: item.CategoryIds, CategoryBinds: toProtoCategoryBinds(playletmod.EffectiveCategoryBinds(item)), CategoryNames: categoryNamesFromMap(item.CategoryIds, categoryNameMap), ThemeIds: item.ThemeIds, ThemeNames: themeNamesFromMap(item.ThemeIds, themeNameMap), TagIds: item.TagIds, TagNames: tagNamesFromMap(item.TagIds, tagNameMap), OldTags: normalizePlayletOldTags(item.OldTags), ContentType: item.ContentType, PayType: item.PayType, SingleCoins: item.SingleCoins, TotalEpisode: item.TotalEpisode, FreeEpisodeCount: item.EffectiveFreeEpisodeCount(), UpdateStatus: item.UpdateStatus, TopRank: item.TopRank, RecommendRank: item.RecommendRank, CountComment: item.CountComment, CountBuy: item.CountBuy, CountBrowse: item.CountBrowse, VirtualCountBrowse: item.VirtualCountBrowse, DisplayCountBrowse: item.CountBrowse + item.VirtualCountBrowse, HeatValue: item.HeatValue, VirtualHeatValue: item.VirtualHeatValue, DisplayHeatValue: item.HeatValue + item.VirtualHeatValue, CountCollect: item.CountCollect, CountLike: item.CountLike, VirtualCountLike: item.VirtualCountLike, DisplayCountLike: playletDisplayCountLike(item), CountDisLike: item.CountDisLike, SeoInfo: normalizeSeoValue(item.SeoInfo), OperatorId: item.OperatorId, OperatorName: item.OperatorName, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, } } func validOfficialFreeEpisodeCount(count int64) bool { return count == -1 || count >= 1 && count <= 1000 } func playletDisplayCountLike(item *playletmod.Playlet) int64 { if item == nil { return 0 } return item.CountLike + item.VirtualCountLike } func chapterToWebResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterListResp { if item == nil { return nil } return &proto.PlayletChapterListResp{ Id: item.Id, PlayletId: item.PlayletId, MediaContentId: item.MediaContentId, CurrentEpisode: item.CurrentEpisode, Title: item.Title, Md5: item.Md5, Hash: item.Hash, Height: item.Height, Width: item.Width, Ratio: item.Ratio, VideoUrl: item.VideoUrl, WatermarkVideoUrl: item.WatermarkVideoUrl, MediaSize: item.MediaSize, MediaTime: item.MediaTime, CountComment: item.CountComment, CountBuy: item.CountBuy, CountBrowse: item.CountBrowse, CountCollect: item.CountCollect, CountLike: item.CountLike, CountDisLike: item.CountDisLike, PayType: item.PayType, SingleCoins: item.SingleCoins, WatermarkStatus: item.WatermarkStatus, MediaSourceId: item.MediaSourceId, MediaTaskId: item.MediaTaskId, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, } } func buildCategoryNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) { categoryIds := make([]int64, 0) seen := make(map[int64]struct{}) for _, item := range list { if item == nil { continue } for _, categoryId := range item.CategoryIds { if categoryId <= 0 { continue } if _, ok := seen[categoryId]; ok { continue } seen[categoryId] = struct{}{} categoryIds = append(categoryIds, categoryId) } } result := make(map[int64]string, len(categoryIds)) if len(categoryIds) == 0 { return result, nil } categories, err := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": categoryIds}, }, options.Find().SetProjection(bson.M{ playletcategorymod.FN.PlayletCategory.Id: 1, playletcategorymod.FN.PlayletCategory.Name: 1, })) if err != nil { return nil, err } for _, category := range categories { if category != nil { result[category.Id] = category.Name } } return result, nil } func categoryNamesFromMap(categoryIds []int64, categoryNameMap map[int64]string) []string { result := make([]string, 0, len(categoryIds)) for _, categoryId := range categoryIds { if name := categoryNameMap[categoryId]; name != "" { result = append(result, name) } } return result } func buildThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) { themeIds := make([]int64, 0) seen := make(map[int64]struct{}) for _, item := range list { if item == nil { continue } for _, themeId := range item.ThemeIds { if themeId <= 0 { continue } if _, ok := seen[themeId]; ok { continue } seen[themeId] = struct{}{} themeIds = append(themeIds, themeId) } } result := make(map[int64]string, len(themeIds)) if len(themeIds) == 0 { return result, nil } themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds}, }, options.Find().SetProjection(bson.M{ playletthememod.FN.PlayletTheme.Id: 1, playletthememod.FN.PlayletTheme.Name: 1, })) if err != nil { return nil, err } for _, theme := range themes { if theme != nil { result[theme.Id] = theme.Name } } return result, nil } func themeNamesFromMap(themeIds []int64, themeNameMap map[int64]string) []string { result := make([]string, 0, len(themeIds)) for _, themeId := range themeIds { if name := themeNameMap[themeId]; name != "" { result = append(result, name) } } return result } func buildTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) { tagIds := make([]int64, 0) seen := make(map[int64]struct{}) for _, item := range list { if item == nil { continue } for _, tagId := range item.TagIds { if tagId <= 0 { continue } if _, ok := seen[tagId]; ok { continue } seen[tagId] = struct{}{} tagIds = append(tagIds, tagId) } } result := make(map[int64]string, len(tagIds)) if len(tagIds) == 0 { return result, nil } tags, err := contenttagmod.GetMapTags(ctx, tagIds) if err != nil { return nil, err } for _, tag := range tags { if tag != nil { result[tag.Id] = tag.Name } } return result, nil } func tagNamesFromMap(tagIds []int64, tagNameMap map[int64]string) []string { result := make([]string, 0, len(tagIds)) for _, tagId := range tagIds { if name := tagNameMap[tagId]; name != "" { result = append(result, name) } } return result } func normalizePlayletOldTags(oldTags []string) []string { if oldTags == nil { return make([]string, 0) } return oldTags } func dedupePositiveInt64Ids(ids []int64) []int64 { result := make([]int64, 0, len(ids)) seen := make(map[int64]struct{}, len(ids)) for _, id := range ids { if id <= 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} result = append(result, id) } return result } func isValidPlayletStatus(status playletmod.Status) bool { return status == playletmod.StatusDown || status == playletmod.StatusUp } func isValidPlayletPayType(payType playletmod.PayType) bool { return payType == playletmod.PayTypeFree || payType == playletmod.PayTypeCoin || payType == playletmod.PayTypeVip } func isValidPlayletChapterPayType(payType playletchaptermod.PayType) bool { return payType == playletchaptermod.PayTypeFree || payType == playletchaptermod.PayTypeCoin || payType == playletchaptermod.PayTypeVip } func isValidPlayletContentType(contentType playletmod.ContentType) bool { return contentType == playletmod.ContentTypeTheater || contentType == playletmod.ContentTypeAlbum || contentType == playletmod.ContentTypeComic || contentType == playletmod.ContentTypeAnime || contentType == playletmod.ContentTypeManga } func normalizePlayletPaySetting(payType playletmod.PayType, singleCoins *int64, requireCoinAmount bool) (playletmod.PayType, int64, *srvdef.SrvErr) { if !isValidPlayletPayType(payType) { return 0, 0, srvdef.New(srvdef.ErrParamError, "支付方式无效", "") } if payType == playletmod.PayTypeCoin { if requireCoinAmount && (singleCoins == nil || *singleCoins <= 0) { return 0, 0, srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "") } if singleCoins == nil { return payType, 0, nil } return payType, *singleCoins, nil } return payType, 0, nil } func normalizePlayletTagIds(tagIds []int64) ([]int64, *srvdef.SrvErr) { result := make([]int64, 0, len(tagIds)) seen := make(map[int64]struct{}, len(tagIds)) for _, tagId := range tagIds { if tagId <= 0 { return nil, srvdef.New(srvdef.ErrParamError, "标签ID无效", "") } if _, ok := seen[tagId]; ok { continue } seen[tagId] = struct{}{} result = append(result, tagId) } return result, nil } func validatePlayletTagIds(ctx context.Context, tagIds []int64) *srvdef.SrvErr { if len(tagIds) == 0 { return nil } return srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet) } // resolveAddCategoryBinds 新增短剧时根据分类绑定或分类ID生成排序配置 func resolveAddCategoryBinds(binds []proto.PlayletCategoryBind, ids []int64) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) { if len(binds) > 0 { return resolveCategoryBinds(&binds, nil, nil) } return resolveCategoryBinds(nil, &ids, nil) } // resolveCategoryBinds 保留旧分类排序,新增分类默认排序为0 func resolveCategoryBinds(binds *[]proto.PlayletCategoryBind, ids *[]int64, old []playletmod.PlayletCategoryBind) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) { if binds != nil { result := make([]playletmod.PlayletCategoryBind, 0, len(*binds)) seen := make(map[int64]bool, len(*binds)) for _, bind := range *binds { if bind.Id <= 0 || seen[bind.Id] { return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "") } seen[bind.Id] = true result = append(result, playletmod.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank}) } return result, nil } if ids != nil { categoryIds, err := normalizePlayletCategoryIds(*ids) if err != nil { return nil, err } return playletmod.MergeCategoryBindsByIds(old, categoryIds), nil } return nil, nil } func toProtoCategoryBinds(binds []playletmod.PlayletCategoryBind) []proto.PlayletCategoryBind { result := make([]proto.PlayletCategoryBind, 0, len(binds)) for _, bind := range binds { result = append(result, proto.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank}) } return result } func normalizePlayletCategoryIds(categoryIds []int64) ([]int64, *srvdef.SrvErr) { result := make([]int64, 0, len(categoryIds)) seen := make(map[int64]struct{}, len(categoryIds)) for _, categoryId := range categoryIds { if categoryId <= 0 { return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "") } if _, ok := seen[categoryId]; ok { continue } seen[categoryId] = struct{}{} result = append(result, categoryId) } return result, nil } func validatePlayletCategoryIds(ctx context.Context, categoryIds []int64) *srvdef.SrvErr { if len(categoryIds) == 0 { return nil } count, err := playletcategorymod.CountPlayletCategory(ctx, bson.M{ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": categoryIds}, }) if err != nil { return err } if count != int64(len(categoryIds)) { return srvdef.New(srvdef.ErrParamError, "分类不存在", "") } return nil } func validatePlayletThemeIds(ctx context.Context, themeIds []int64) *srvdef.SrvErr { if len(themeIds) == 0 { return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "") } count, err := playletthememod.CountPlayletTheme(ctx, bson.M{ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds}, }) if err != nil { return err } if count != int64(len(themeIds)) { return srvdef.New(srvdef.ErrParamError, "题材不存在", "") } return nil } func playletOnlineAt(status playletmod.Status, now time.Time) time.Time { if status == playletmod.StatusUp { return now } return time.Time{} } func resolvePlayletOperator(ctx context.Context, operatorId int64) (int64, string) { if operatorId <= 0 { return 0, "" } acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{ mwacc.FN.MwAcc.Id: operatorId, }, options.FindOne().SetProjection(bson.D{ {Key: mwacc.FN.MwAcc.Id, Value: 1}, {Key: mwacc.FN.MwAcc.UserName, Value: 1}, })) if err != nil || acc == nil { return operatorId, "" } return acc.Id, acc.UserName } func asyncSyncPlayletESByStatus(ids []int64, status playletmod.Status, action string) { syncIds := append([]int64(nil), ids...) go func() { if err1 := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, status); err1 != nil { log.Error(action+"同步ES失败", log.Any("短剧ID", syncIds), log.Any("状态", status), log.E(err1)) } }() } func asyncSyncPlayletESByLatestStatus(ids []int64, action string) { syncIds := append([]int64(nil), ids...) go func() { if err1 := srv_search.SyncPlayletsToESByLatestStatus(context.Background(), syncIds); err1 != nil { log.Error(action+"同步ES失败", log.Any("短剧ID", syncIds), log.E(err1)) } }() } func randomUniqueVirtualLikeValues(min int64, max int64, count int) []int64 { if count <= 0 { return nil } rng := rand.New(rand.NewSource(time.Now().UnixNano())) result := make([]int64, 0, count) selected := make(map[int64]struct{}, count) start := max - int64(count) + 1 for offset := int64(0); offset < int64(count); offset++ { current := start + offset picked := min + rng.Int63n(current-min+1) if _, ok := selected[picked]; ok { picked = current } selected[picked] = struct{}{} result = append(result, picked) } return result }