Files
web_hgdjb_server/web/service/srv_playlet/playlet.go
T
rootandClaude Opus 4.7 cd8dea8787 同步 web_hga 的五项改造
- 移除 买点 (user acquisition tracking) 子系统:dataReport SDK、eventmod
  事件模型、reportdata 定时上报 job、reportser/srv_report 包装层,以及业务
  侧的 ~20 处 Report* 调用点;去掉 proto.ConfigInfo 的 ReportSDKConfig
  字段、DataReport yaml 配置块和 pubdef.TrackerAppId。
- 广告获取改走 versionUrl /api/stat/advitise/v2/get(appId=12),移除
  adcenter(Jt) 调用;新增 POST /api/app/ads/click 异步上报;
  proto.AdvitiseData 别名 proxy.AdvitiseData,AList/BList 改名
  AppInfoAList/AppInfoBList。
- web 后台登录取消谷歌验证码(srv_mwacc.AccLogin 去掉 code 参数),密码
  正确即发放 token。
- app 端取消注册/登录/找回密码/游客登录接口及密码、token 相关代码;鉴权
  中间件按 X-User-Agent 中的 devID 识别用户,首次访问自动建号,Redis 缓存
  10 分钟;修改资料不再支持改密码。
- app m3u8 ticket 校验失败不再降级到 fallback 路径,仍按原 m3u8Name 返回。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-11-07 11:22:25 +08:00

1925 lines
63 KiB
Go
Raw 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 srv_playlet
import (
"context"
"math/rand"
"regexp"
"strings"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/response"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/playletchaptercollectmod"
"videoweb/pub/models/playletchapterlikemod"
"videoweb/pub/models/playletchaptermod"
"videoweb/pub/models/playletchapterunlockmod"
"videoweb/pub/models/playletmod"
"videoweb/pub/models/playlettagmod"
"videoweb/pub/models/playletthememod"
"videoweb/pub/pubdef"
"videoweb/web/api/proto"
"videoweb/web/service/srv_aicomment"
"videoweb/web/service/srv_mediaresource"
"videoweb/web/service/srv_search"
"videoweb/web/skd/playletwatermark"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
defaultPageSize int64 = 10
maxPageSize int64 = 100
)
// 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 && req.SortType == proto.PlayletListSortRecommendRank {
list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, *req.CategoryId, (pageNum-1)*pageSize, pageSize, playletmod.CategoryRankAdminSort(), nil)
} else {
list, err = playletmod.FindManyPlaylet(ctx, filter, options.Find().
SetSort(buildPlayletListSort(req.SortType)).
SetSkip((pageNum-1)*pageSize).
SetLimit(pageSize))
}
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
}
items = append(items, playletToWebResp(item, themeNameMap, tagNameMap, req.CategoryId))
}
resp.List = items
return resp, nil
}
// AddPlaylet 新增短剧
func AddPlaylet(ctx context.Context, req *proto.PlayletAddReq) (*proto.PlayletAddResp, *srvdef.SrvErr) {
if req == nil || strings.TrimSpace(req.Title) == "" {
return nil, srvdef.New(srvdef.ErrParamError, "标题不能为空", "")
}
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
}
banner := strings.TrimSpace(req.Banner)
if req.ShowOnBanner && banner == "" {
return nil, srvdef.New(srvdef.ErrParamError, "显示在banner时,banner图不能为空", "")
}
if err := validatePlayletTagIds(ctx, req.TagIds); err != nil {
return nil, err
}
categoryBinds, err := resolveAddPlayletCategoryBinds(req.CategoryBinds, req.CategoryIds)
if err != nil {
return nil, err
}
payType, singleCoins, srvErr := normalizePlayletPaySetting(req.PayType, &req.SingleCoins, true)
if srvErr != nil {
return nil, srvErr
}
operateType, srvErr := normalizePlayletOperateType(req.OperateType, true)
if srvErr != nil {
return nil, srvErr
}
doc := &playletmod.Playlet{
ThemeIds: themeIds,
CategoryIds: playletmod.CategoryIdsFromBinds(categoryBinds),
CategoryBinds: categoryBinds,
Title: strings.TrimSpace(req.Title),
PlayName: strings.TrimSpace(req.PlayName),
Slug: strings.TrimSpace(req.Slug),
Desc: strings.TrimSpace(req.Desc),
Anchor: strings.TrimSpace(req.Anchor),
Cover: req.Cover,
HorizontalCover: strings.TrimSpace(req.HorizontalCover),
Preview: strings.TrimSpace(req.Preview),
TagIds: req.TagIds,
ContentType: req.ContentType,
OperateType: operateType,
PayType: payType,
SingleCoins: singleCoins,
TotalEpisode: req.TotalEpisode,
TopRank: req.TopRank,
RecommendRank: req.RecommendRank,
Status: req.Status,
IsFeatured: req.IsFeatured,
IsOriginal: true, // 管理后台新增的短剧都是原创
ShowOriginal: req.ShowOriginal,
IsRecommend: req.IsRecommend,
ShowOnBanner: req.ShowOnBanner,
Banner: banner,
MobileBanner: strings.TrimSpace(req.MobileBanner),
SeoInfo: normalizeSeoValue(req.SeoInfo),
}
applyPlayletDescription(doc)
if doc.Status == playletmod.StatusUp {
doc.OnlineAt = resolvePlayletOnlineAt(req.OnlineAt, time.Now())
}
if doc.TagIds == nil {
doc.TagIds = make([]int64, 0)
}
if doc.CategoryBinds == nil {
doc.CategoryBinds = make([]playletmod.PlayletCategoryBind, 0)
}
doc.CategoryIds = playletmod.CategoryIdsFromBinds(doc.CategoryBinds)
applyPlayletOperatorToDoc(ctx, doc)
if err := ensurePlayletSlug(ctx, doc, 0); err != nil {
return nil, err
}
if err := playletmod.InsertOnePlaylet(ctx, doc); err != nil {
return nil, err
}
go func() {
srv_mediaresource.FillPlayletCompressedCover(context.Background(), doc)
srv_mediaresource.FillPlayletCompressedHorizontalCover(context.Background(), doc)
}()
clearAppPlayletHomeCache()
notifyPlayletSeoAfterWrite(ctx, doc.Id, nil)
// 上架短剧异步写入 ES(含 SEO),避免阻塞后台接口。
if doc.Status == playletmod.StatusUp {
syncId := doc.Id
submitPlayletAiComments(syncId)
go func() {
if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{syncId}, playletmod.StatusUp); err != nil {
log.Error("短剧新增同步ES失败", log.Any("短剧ID", syncId), log.E(err))
}
}()
}
return &proto.PlayletAddResp{Id: doc.Id}, nil
}
// UpdatePlaylet 更新短剧
func UpdatePlaylet(ctx context.Context, req *proto.PlayletUpdateReq) *srvdef.SrvErr {
if req == nil || req.Id <= 0 {
return srvdef.New(srvdef.ErrParamError, "id无效", "")
}
if req.ShowOnBanner != nil || req.Banner != nil {
exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
if err != nil {
return err
}
showOnBanner := exist.ShowOnBanner
banner := exist.Banner
if req.ShowOnBanner != nil {
showOnBanner = *req.ShowOnBanner
}
if req.Banner != nil {
banner = strings.TrimSpace(*req.Banner)
}
if showOnBanner && banner == "" {
return srvdef.New(srvdef.ErrParamError, "显示在banner时,banner图不能为空", "")
}
}
exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
if err != nil {
return err
}
setDoc := bson.M{playletmod.FN.Playlet.UpdatedAt: time.Now()}
if req.Status != nil {
setDoc[playletmod.FN.Playlet.Status] = *req.Status
setDoc[playletmod.FN.Playlet.ReviewAt] = time.Now()
}
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.MobileBanner != nil {
setDoc[playletmod.FN.Playlet.MobileBanner] = strings.TrimSpace(*req.MobileBanner)
}
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.CategoryBinds != nil || req.CategoryIds != nil {
categoryBinds, serr := resolvePlayletCategoryBinds(req.CategoryBinds, req.CategoryIds, playletmod.EffectiveCategoryBinds(exist))
if serr != nil {
return serr
}
setDoc[playletmod.FN.Playlet.CategoryBinds] = categoryBinds
setDoc[playletmod.FN.Playlet.CategoryIds] = playletmod.CategoryIdsFromBinds(categoryBinds)
}
if req.Title != nil {
setDoc[playletmod.FN.Playlet.Title] = strings.TrimSpace(*req.Title)
}
if req.PlayName != nil {
setDoc[playletmod.FN.Playlet.PlayName] = strings.TrimSpace(*req.PlayName)
}
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
horizontalCoverChanged := false
if req.Cover != nil {
newCover := strings.TrimSpace(*req.Cover)
setDoc[playletmod.FN.Playlet.Cover] = newCover
exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
if err != nil {
return err
}
if exist == nil || strings.TrimSpace(exist.Cover) != newCover {
// 封面变更后清空压缩封面,由后台任务按新原图重新压缩。
setDoc[playletmod.FN.Playlet.CompressedCover] = ""
coverChanged = true
}
}
if req.HorizontalCover != nil {
newHorizontalCover := strings.TrimSpace(*req.HorizontalCover)
setDoc[playletmod.FN.Playlet.HorizontalCover] = newHorizontalCover
exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
if err != nil {
return err
}
if exist == nil || strings.TrimSpace(exist.HorizontalCover) != newHorizontalCover {
setDoc[playletmod.FN.Playlet.CompressedHorizontalCover] = ""
horizontalCoverChanged = true
} else if newHorizontalCover != "" && strings.TrimSpace(exist.CompressedHorizontalCover) == "" {
horizontalCoverChanged = true
}
}
if req.Preview != nil {
setDoc[playletmod.FN.Playlet.Preview] = strings.TrimSpace(*req.Preview)
}
if req.TagIds != nil {
if err := validatePlayletTagIds(ctx, *req.TagIds); err != nil {
return err
}
setDoc[playletmod.FN.Playlet.TagIds] = *req.TagIds
}
if req.ContentType != nil {
setDoc[playletmod.FN.Playlet.ContentType] = *req.ContentType
}
if req.OperateType != nil {
operateType, srvErr := normalizePlayletOperateType(*req.OperateType, false)
if srvErr != nil {
return srvErr
}
setDoc[playletmod.FN.Playlet.OperateType] = operateType
}
if req.PayType != nil || req.SingleCoins != nil {
existPayType := playletmod.PayTypeFree
existCoins := int64(0)
if req.PayType == nil || req.SingleCoins == nil {
exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
if err != nil {
return err
}
existPayType = exist.PayType
existCoins = exist.SingleCoins
}
payType := existPayType
if req.PayType != nil {
payType = *req.PayType
}
coins := &existCoins
if req.SingleCoins != nil {
coins = req.SingleCoins
}
normalizedPayType, normalizedCoins, srvErr := normalizePlayletPaySetting(payType, coins, true)
if srvErr != nil {
return srvErr
}
setDoc[playletmod.FN.Playlet.PayType] = normalizedPayType
setDoc[playletmod.FN.Playlet.SingleCoins] = normalizedCoins
}
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)
}
var prevStatus *playletmod.Status
if exist != nil {
st := exist.Status
prevStatus = &st
}
applyPlayletOnlineAtOnUpdate(exist, req, setDoc, time.Now())
if err := preparePlayletSlugOnUpdate(ctx, req.Id, exist, req, setDoc); err != nil {
return err
}
applyPlayletDescriptionOnUpdate(exist, req, setDoc)
if len(setDoc) <= 1 {
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
if err := playletmod.UpdateOnePlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: req.Id},
bson.M{"$set": setDoc},
); 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))
}
if horizontalCoverChanged {
go func(playletId int64, cover string) {
srv_mediaresource.FillPlayletCompressedHorizontalCover(context.Background(), &playletmod.Playlet{
Id: playletId,
HorizontalCover: cover,
})
}(req.Id, strings.TrimSpace(*req.HorizontalCover))
}
clearAppPlayletHomeCache()
notifyPlayletSeoAfterWrite(ctx, req.Id, prevStatus)
if req.Status != nil && *req.Status == playletmod.StatusUp {
submitPlayletAiComments(req.Id)
}
// 编辑后按当前上下架状态异步同步 ES(含 SEO、题材名等搜索字段)。
syncId := req.Id
go func() {
exist, err := playletmod.FindOnePlaylet(context.Background(), bson.M{
playletmod.FN.Playlet.Id: syncId,
}, options.FindOne().SetProjection(bson.D{
{Key: playletmod.FN.Playlet.Id, Value: 1},
{Key: playletmod.FN.Playlet.Status, Value: 1},
}))
if err != nil || exist == nil {
if err != nil {
log.Error("短剧更新后查询状态失败,跳过ES同步", log.Any("短剧ID", syncId), log.E(err))
}
return
}
if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{syncId}, exist.Status); err != nil {
log.Error("短剧更新同步ES失败", log.Any("短剧ID", syncId), log.Any("状态", exist.Status), log.E(err))
}
}()
return nil
}
// DelPlaylet 单/批量物理删除短剧及其章节
func DelPlaylet(ctx context.Context, req *proto.PlayletDelReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
ids := dedupePositiveInt64Ids(req.Ids)
if len(ids) == 0 {
return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
}
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.Status, Value: 1},
}))
if err != nil {
return err
}
if len(list) == 0 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
deleteIds := make([]int64, 0, len(list))
onlineIds := make([]int64, 0, len(list))
for _, item := range list {
if item == nil || item.Id <= 0 {
continue
}
deleteIds = append(deleteIds, item.Id)
if item.Status == playletmod.StatusUp {
onlineIds = append(onlineIds, item.Id)
}
}
if len(deleteIds) == 0 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
deletedPlayletCount, err := playletmod.DelManyPlaylet(ctx, bson.M{
playletmod.FN.Playlet.Id: bson.M{"$in": deleteIds},
})
if err != nil {
return err
}
if deletedPlayletCount == 0 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
if _, err := playletchaptermod.DelManyPlayletChapter(ctx, bson.M{
playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": deleteIds},
}); err != nil {
return err
}
clearAppPlayletHomeCache()
notifyPlayletSeoAfterDelete(ctx, onlineIds)
syncIds := append([]int64(nil), deleteIds...)
go func() {
if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, playletmod.StatusDown); err != nil {
log.Error("短剧物理删除同步ES失败", log.Any("短剧ID", syncIds), log.E(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 {
// 传入上架时间则整批使用该时间。未传时不在这里覆盖,避免改掉已有上架时间。
if onlineAt, ok := explicitPlayletOnlineAt(req.OnlineAt); ok {
setDoc[playletmod.FN.Playlet.OnlineAt] = onlineAt
}
setDoc[playletmod.FN.Playlet.IndexNow] = int64(0)
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
// 单条和批量都走 Mongo 批量更新,避免批量操作时逐条访问数据库。
if err := playletmod.UpdateManyPlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
bson.M{"$set": setDoc},
); err != nil {
return err
}
if req.Status == playletmod.StatusUp {
if _, ok := explicitPlayletOnlineAt(req.OnlineAt); !ok {
if err := fillUnsetPlayletOnlineAt(ctx, ids, now); err != nil {
return err
}
}
}
clearAppPlayletHomeCache()
notifyPlayletSeoAfterStatus(ctx, ids, req.Status, now)
// 上架会同步写入 ES,ES Bulk 带 refresh 且无超时,同步等待会把接口卡住。
// Mongo 已更新成功后异步同步 ES,避免 status=1 时接口无响应。
syncIds := append([]int64(nil), ids...)
syncStatus := req.Status
go func() {
if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, syncStatus); err != nil {
log.Error("短剧状态同步ES失败", log.Any("短剧ID", syncIds), log.Any("状态", syncStatus), log.E(err))
}
}()
if req.Status == playletmod.StatusUp {
submitPlayletAiComments(ids...)
}
return nil
}
func submitPlayletAiComments(ids ...int64) {
playletIDs := dedupePositiveInt64Ids(ids)
if len(playletIDs) == 0 {
return
}
go func() {
if err := srv_aicomment.AutoProcessUnsubmittedPlaylets(context.Background(), playletIDs); err != nil {
log.Warn("提交短剧自动评论任务失败", log.E(err), log.Any("playletIDs", playletIDs))
}
}()
}
// 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不能为空", "")
}
// isRecommend 是明确的 bool 开关,false 表示取消推荐。
setDoc := bson.M{
playletmod.FN.Playlet.IsRecommend: req.IsRecommend,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
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
}
// 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不能为空", "")
}
// isFeatured 是爆剧开关,直接批量写入请求值,false 表示取消爆剧。
setDoc := bson.M{
playletmod.FN.Playlet.IsFeatured: req.IsFeatured,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
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
}
// SetPlayletOperateType 单/批量设置短剧运营类型
func SetPlayletOperateType(ctx context.Context, req *proto.PlayletOperateTypeSetReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
ids := dedupePositiveInt64Ids(req.Ids)
if len(ids) == 0 {
return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
}
operateType, srvErr := normalizePlayletOperateType(req.OperateType, false)
if srvErr != nil {
return srvErr
}
setDoc := bson.M{
playletmod.FN.Playlet.OperateType: operateType,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
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
}
// 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不能为空", "")
}
// showOnBanner 是首页banner展示开关,只负责批量设置字段值。
setDoc := bson.M{
playletmod.FN.Playlet.ShowOnBanner: req.ShowOnBanner,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
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
}
// 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
}
// themeIds 是短剧多题材字段,按请求覆盖原题材集合。
setDoc := bson.M{
playletmod.FN.Playlet.ThemeIds: themeIds,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
if err := playletmod.UpdateManyPlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
bson.M{"$set": setDoc},
); err != nil {
return err
}
clearAppPlayletHomeCache()
markSitemapDirty(ctx)
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 := normalizePlayletTagIds(*req.TagIds)
if err != nil {
return err
}
if err := validatePlayletTagIds(ctx, tagIds); err != nil {
return err
}
// tagIds 是短剧多标签字段,按请求覆盖原标签集合;空数组表示清空标签。
setDoc := bson.M{
playletmod.FN.Playlet.TagIds: tagIds,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
if err := playletmod.UpdateManyPlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
bson.M{"$set": setDoc},
); err != nil {
return err
}
clearAppPlayletHomeCache()
markSitemapDirty(ctx)
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.CategoryBinds == nil && req.CategoryIds == nil {
return srvdef.New(srvdef.ErrParamError, "分类不能为空", "")
}
categoryBinds, err := resolvePlayletCategoryBinds(req.CategoryBinds, req.CategoryIds, nil)
if err != nil {
return err
}
setDoc := bson.M{
playletmod.FN.Playlet.CategoryBinds: categoryBinds,
playletmod.FN.Playlet.CategoryIds: playletmod.CategoryIdsFromBinds(categoryBinds),
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
if err := playletmod.UpdateManyPlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
bson.M{"$set": setDoc},
); err != nil {
return err
}
clearAppPlayletHomeCache()
markSitemapDirty(ctx)
return nil
}
// SetPlayletVirtualLike 单/批量设置短剧虚拟点赞数
func SetPlayletVirtualLike(ctx context.Context, req *proto.PlayletVirtualLikeSetReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
// 去重并过滤无效ID,避免同一短剧在一次批量请求中被重复赋值。
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,
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
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()
syncIds := append([]int64(nil), ids...)
go func() {
// 虚拟点赞数会写入 ES 的展示点赞数字段,后台保存成功后异步刷新 ES。
if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, playletmod.StatusUp); err != nil {
log.Error("短剧虚拟点赞数同步ES失败", log.Any("短剧ID", syncIds), log.E(err))
}
}()
return nil
}
// randomUniqueVirtualLikeValues 从[min,max]范围内随机取count个不重复的虚拟点赞数
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)
// 使用Floyd采样算法,避免在大范围随机时为了去重反复重试。
if _, ok := selected[picked]; ok {
selected[current] = struct{}{}
result = append(result, current)
continue
}
selected[picked] = struct{}{}
result = append(result, picked)
}
rng.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
return result
}
// 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.Status != nil {
filter[playletchaptermod.FN.PlayletChapter.Status] = *req.Status
}
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无效", "")
}
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: req.Md5,
Hash: 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: req.SingleCoins,
Status: playletchaptermod.DefaultStatusByMediaSource(playlet.MediaSource),
}
if req.Status != nil {
if !isValidPlayletChapterStatus(*req.Status) {
return nil, srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
}
doc.Status = *req.Status
}
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
}
// 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 {
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] = *req.Md5
}
if req.Hash != nil {
setDoc[playletchaptermod.FN.PlayletChapter.Hash] = *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] = *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 {
setDoc[playletchaptermod.FN.PlayletChapter.PayType] = *req.PayType
}
if req.SingleCoins != nil {
setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = *req.SingleCoins
}
if req.Status != nil {
if !isValidPlayletChapterStatus(*req.Status) {
return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
}
setDoc[playletchaptermod.FN.PlayletChapter.Status] = *req.Status
}
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},
)
}
// 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, "", "")
}
if _, serr := playletchapterlikemod.DeleteManyPlayletChapterLike(ctx, bson.M{
playletchapterlikemod.FN.PlayletChapterLike.ChapterId: bson.M{"$in": ids},
}); serr != nil {
log.Warn("删除剧集后清理点赞关系失败", log.Any("chapterIds", ids), log.E(serr))
}
if _, serr := playletchaptercollectmod.DeleteManyPlayletChapterCollect(ctx, bson.M{
playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: bson.M{"$in": ids},
}); serr != nil {
log.Warn("删除剧集后清理收藏关系失败", log.Any("chapterIds", ids), log.E(serr))
}
if _, serr := playletchapterunlockmod.DeleteManyPlayletChapterUnlock(ctx, bson.M{
playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId: bson.M{"$in": ids},
}); serr != nil {
log.Warn("删除剧集后清理解锁记录失败", log.Any("chapterIds", ids), log.E(serr))
}
return nil
}
// FillPlayletChapterStatusUp 将未写入上架状态的历史剧集设为上架
func FillPlayletChapterStatusUp(ctx context.Context) (*proto.PlayletChapterStatusFillResp, *srvdef.SrvErr) {
filter := bson.M{
"$or": []bson.M{
{playletchaptermod.FN.PlayletChapter.Status: bson.M{"$exists": false}},
{playletchaptermod.FN.PlayletChapter.Status: nil},
},
}
count, err := playletchaptermod.CountPlayletChapter(ctx, filter)
if err != nil {
return nil, err
}
resp := &proto.PlayletChapterStatusFillResp{}
if count == 0 {
return resp, nil
}
if err := playletchaptermod.UpdateManyPlayletChapter(ctx, filter, bson.M{
"$set": bson.M{playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp},
}); err != nil {
return nil, err
}
resp.UpdatedCount = count
return resp, nil
}
// SetPlayletChapterStatus 单/批量设置章节上架状态
func SetPlayletChapterStatus(ctx context.Context, req *proto.PlayletChapterStatusSetReq) *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 !isValidPlayletChapterStatus(req.Status) {
return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
}
return playletchaptermod.UpdateManyPlayletChapter(ctx,
bson.M{playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids}},
bson.M{"$set": bson.M{
playletchaptermod.FN.PlayletChapter.Status: req.Status,
playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now(),
}},
)
}
// 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},
)
}
// 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, srvErr := normalizePlayletPaySetting(req.PayType, req.SingleCoins, true)
if srvErr != nil {
return srvErr
}
setDoc := bson.M{
playletmod.FN.Playlet.PayType: payType,
playletmod.FN.Playlet.SingleCoins: singleCoins,
playletmod.FN.Playlet.UpdatedAt: time.Now(),
}
applyPlayletOperatorToSetDoc(ctx, setDoc)
return playletmod.UpdateManyPlaylet(ctx,
bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
bson.M{"$set": setDoc},
)
}
// 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,
},
"$unset": bson.M{
playletchaptermod.FN.PlayletChapter.WatermarkRetryAt: "",
},
},
)
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,
},
"$unset": bson.M{
playletchaptermod.FN.PlayletChapter.WatermarkRetryAt: "",
},
},
)
if err != nil && err.Code == srvdef.ErrNoData {
return nil
}
return err
}
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.ThemeId != nil {
filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{*req.ThemeId}}
}
if req.CategoryId != nil {
filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{*req.CategoryId}}
}
if req.TagId != nil {
filter[playletmod.FN.Playlet.TagIds] = bson.M{"$in": []int64{*req.TagId}}
}
if req.ContentType != nil {
filter[playletmod.FN.Playlet.ContentType] = *req.ContentType
}
if req.OperateType != nil {
if *req.OperateType == playletmod.OperateTypeComic {
filter[playletmod.FN.Playlet.OperateType] = playletmod.OperateTypeComic
} else {
// 历史数据可能没有该字段,按短剧处理
filter[playletmod.FN.Playlet.OperateType] = bson.M{"$ne": playletmod.OperateTypeComic}
}
}
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 slug := strings.TrimSpace(req.Slug); slug != "" {
filter[playletmod.FN.Playlet.Slug] = slug
}
if operatorName := strings.TrimSpace(req.OperatorName); operatorName != "" {
filter[playletmod.FN.Playlet.OperatorName] = bson.M{"$regex": regexp.QuoteMeta(operatorName)}
}
if req.MediaSource != nil {
mediaSource := strings.TrimSpace(*req.MediaSource)
if mediaSource == playletmod.MediaSourceHG {
filter[playletmod.FN.Playlet.MediaSource] = playletmod.MediaSourceHG
} else if mediaSource == playletmod.MediaSourceDefault {
filter["$or"] = []bson.M{
{playletmod.FN.Playlet.MediaSource: playletmod.MediaSourceDefault},
{playletmod.FN.Playlet.MediaSource: bson.M{"$exists": false}},
}
} else {
filter[playletmod.FN.Playlet.MediaSource] = mediaSource
}
}
return filter
}
// dedupePositiveInt64Ids 过滤无效ID并去重
func dedupePositiveInt64Ids(ids []int64) []int64 {
result := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
// 只有正数ID才是有效的 Mongo 业务自增ID。
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
func resolveAddPlayletCategoryBinds(binds []proto.PlayletCategoryBind, ids []int64) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
if len(binds) > 0 {
return resolvePlayletCategoryBinds(&binds, nil, nil)
}
return resolvePlayletCategoryBinds(nil, &ids, nil)
}
func resolvePlayletCategoryBinds(binds *[]proto.PlayletCategoryBind, ids *[]int64, exist []playletmod.PlayletCategoryBind) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
if binds != nil {
return playletmod.NormalizeCategoryBinds(toModCategoryBinds(*binds)), nil
}
if ids != nil {
categoryIds := dedupePositiveInt64Ids(*ids)
if len(exist) > 0 {
return playletmod.MergeCategoryBindsByIds(exist, categoryIds), nil
}
return playletmod.CategoryBindsFromIds(categoryIds), nil
}
return make([]playletmod.PlayletCategoryBind, 0), nil
}
func toModCategoryBinds(binds []proto.PlayletCategoryBind) []playletmod.PlayletCategoryBind {
result := make([]playletmod.PlayletCategoryBind, 0, len(binds))
for _, bind := range binds {
result = append(result, playletmod.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank})
}
return result
}
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
}
// isValidPlayletStatus 校验短剧上下架状态
func isValidPlayletStatus(status playletmod.Status) bool {
return status == playletmod.StatusDown || status == playletmod.StatusUp
}
// resolvePlayletOnlineAt 上架时间未传或为零时使用当前时间
func resolvePlayletOnlineAt(passed *time.Time, now time.Time) time.Time {
if onlineAt, ok := explicitPlayletOnlineAt(passed); ok {
return onlineAt
}
return now
}
// explicitPlayletOnlineAt 返回请求里显式传入的上架时间。未传或零值视为未传。
func explicitPlayletOnlineAt(passed *time.Time) (time.Time, bool) {
if passed == nil || passed.IsZero() {
return time.Time{}, false
}
return *passed, true
}
// fillUnsetPlayletOnlineAt 给从未设置过上架时间的短剧写入当前时间。已有上架时间的记录不改。
func fillUnsetPlayletOnlineAt(ctx context.Context, ids []int64, now time.Time) *srvdef.SrvErr {
if len(ids) == 0 {
return nil
}
err := playletmod.UpdateManyPlaylet(ctx, bson.M{
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
"$or": unsetPlayletOnlineAtClauses(),
}, bson.M{"$set": bson.M{
playletmod.FN.Playlet.OnlineAt: now,
}})
if err != nil && err.Code == srvdef.ErrNoData {
return nil
}
return err
}
func unsetPlayletOnlineAtClauses() []bson.M {
return []bson.M{
{playletmod.FN.Playlet.OnlineAt: bson.M{"$exists": false}},
{playletmod.FN.Playlet.OnlineAt: nil},
{playletmod.FN.Playlet.OnlineAt: time.Time{}},
}
}
// applyPlayletOnlineAtOnUpdate 编辑短剧时写入上架时间。
// 新上架未传时间则用当前时间;已上架仅在显式传入时更新。
func applyPlayletOnlineAtOnUpdate(exist *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M, now time.Time) {
if req == nil || setDoc == nil {
return
}
goingUp := req.Status != nil && *req.Status == playletmod.StatusUp && (exist == nil || exist.Status != playletmod.StatusUp)
if goingUp {
setDoc[playletmod.FN.Playlet.OnlineAt] = resolvePlayletOnlineAt(req.OnlineAt, now)
setDoc[playletmod.FN.Playlet.IndexNow] = int64(0)
return
}
if req.OnlineAt != nil && !req.OnlineAt.IsZero() {
setDoc[playletmod.FN.Playlet.OnlineAt] = *req.OnlineAt
}
}
// FillEmptyPlayletOnlineAt 将已上架但上架时间为空的短剧,回填为该条修改时间
func FillEmptyPlayletOnlineAt(ctx context.Context) (*proto.PlayletOnlineAtFillResp, *srvdef.SrvErr) {
filter := emptyOnlineAtUpPlayletFilter()
list, err := playletmod.FindManyPlaylet(ctx, filter, options.Find().SetProjection(bson.D{
{Key: playletmod.FN.Playlet.Id, Value: 1},
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
{Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
}))
if err != nil {
return nil, err
}
resp := &proto.PlayletOnlineAtFillResp{}
if len(list) == 0 {
return resp, nil
}
writes := make([]mongo.WriteModel, 0, len(list))
ids := make([]int64, 0, len(list))
for _, item := range list {
if item == nil || item.Id <= 0 {
continue
}
onlineAt := item.UpdatedAt
if onlineAt.IsZero() {
onlineAt = item.CreatedAt
}
if onlineAt.IsZero() {
continue
}
writes = append(writes, mongo.NewUpdateOneModel().
SetFilter(bson.M{playletmod.FN.Playlet.Id: item.Id}).
SetUpdate(bson.M{"$set": bson.M{
playletmod.FN.Playlet.OnlineAt: onlineAt,
}}))
ids = append(ids, item.Id)
}
if len(writes) == 0 {
return resp, nil
}
if err := playletmod.BulkWritePlaylet(ctx, writes); err != nil {
return nil, err
}
resp.UpdatedCount = int64(len(ids))
clearAppPlayletHomeCache()
syncIds := append([]int64(nil), ids...)
go func() {
if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, playletmod.StatusUp); err != nil {
log.Error("回填上架时间同步ES失败", log.Any("短剧ID", syncIds), log.E(err))
}
}()
return resp, nil
}
func emptyOnlineAtUpPlayletFilter() bson.M {
return bson.M{
playletmod.FN.Playlet.Status: playletmod.StatusUp,
"$or": unsetPlayletOnlineAtClauses(),
}
}
// isValidPlayletChapterStatus 校验章节上下架状态
func isValidPlayletChapterStatus(status playletchaptermod.Status) bool {
return status == playletchaptermod.StatusDown || status == playletchaptermod.StatusUp
}
// isValidPlayletChapterPayType 校验章节支付方式
func isValidPlayletChapterPayType(payType playletchaptermod.PayType) bool {
return payType == playletchaptermod.PayTypeFree ||
payType == playletchaptermod.PayTypeCoin ||
payType == playletchaptermod.PayTypeVip
}
func normalizePlayletOperateType(operateType playletmod.OperateType, allowDefault bool) (playletmod.OperateType, *srvdef.SrvErr) {
if operateType == 0 && allowDefault {
return playletmod.OperateTypePlaylet, nil
}
if !playletmod.IsValidOperateType(operateType) {
return 0, srvdef.New(srvdef.ErrParamError, "运营类型无效", "")
}
return operateType, nil
}
// isValidPlayletPayType 校验短剧默认支付方式
func isValidPlayletPayType(payType playletmod.PayType) bool {
return payType == playletmod.PayTypeFree ||
payType == playletmod.PayTypeCoin ||
payType == playletmod.PayTypeVip
}
// normalizePlayletPaySetting 规范化短剧默认支付方式;非金币时单集金币清零。
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
}
// buildPlayletListSort 后台列表排序,均为降序;未传或非法值按创建时间。
func buildPlayletListSort(sortType proto.PlayletListSortType) bson.D {
sortKey := playletmod.FN.Playlet.CreatedAt
switch sortType {
case proto.PlayletListSortRecommendRank:
sortKey = playletmod.FN.Playlet.RecommendRank
case proto.PlayletListSortUpdatedAt:
sortKey = playletmod.FN.Playlet.UpdatedAt
case proto.PlayletListSortCountLike:
sortKey = playletmod.FN.Playlet.CountLike
case proto.PlayletListSortCountBrowse:
sortKey = playletmod.FN.Playlet.CountBrowse
case proto.PlayletListSortCountCollect:
sortKey = playletmod.FN.Playlet.CountCollect
case proto.PlayletListSortOnlineAt:
sortKey = playletmod.FN.Playlet.OnlineAt
}
return bson.D{
{Key: sortKey, Value: -1},
{Key: playletmod.FN.Playlet.Id, Value: -1},
}
}
func normalizePage(pageNum, pageSize int64) (int64, int64) {
if pageNum <= 0 {
pageNum = 1
}
if pageSize <= 0 {
pageSize = defaultPageSize
}
if pageSize > maxPageSize {
pageSize = maxPageSize
}
return pageNum, pageSize
}
// normalizeSeoValue 规范化 SEO 字段,避免 keywords 为 null
func normalizeSeoValue(seo pubdef.SeoValue) pubdef.SeoValue {
seo.Title = strings.TrimSpace(seo.Title)
seo.Description = strings.TrimSpace(seo.Description)
if seo.Keyworld == nil {
seo.Keyworld = make([]string, 0)
return seo
}
words := make([]string, 0, len(seo.Keyworld))
for _, word := range seo.Keyworld {
word = strings.TrimSpace(word)
if word == "" {
continue
}
words = append(words, word)
}
seo.Keyworld = words
return seo
}
func applyPlayletDescription(doc *playletmod.Playlet) {
if doc == nil {
return
}
description := playletmod.BuildPlayNameDescription(doc.Desc, doc.PlayName, doc.UpdateStatus, doc.TotalEpisode)
if description == "" {
return
}
doc.Description = description
doc.SeoInfo.Description = description
}
func applyPlayletDescriptionOnUpdate(exist *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M) {
if exist == nil || req == nil || setDoc == nil {
return
}
playName := strings.TrimSpace(exist.PlayName)
if req.PlayName != nil {
playName = strings.TrimSpace(*req.PlayName)
}
if playName == "" {
return
}
desc := exist.Desc
if req.Desc != nil {
desc = strings.TrimSpace(*req.Desc)
}
totalEpisode := exist.TotalEpisode
if req.TotalEpisode != nil {
totalEpisode = *req.TotalEpisode
}
description := playletmod.BuildPlayNameDescription(desc, playName, exist.UpdateStatus, totalEpisode)
if description == "" {
return
}
setDoc[playletmod.FN.Playlet.Description] = description
seo := exist.SeoInfo
if req.SeoInfo != nil {
if current, ok := setDoc["seoInfo"].(pubdef.SeoValue); ok {
seo = current
} else {
seo = normalizeSeoValue(*req.SeoInfo)
}
}
seo = normalizeSeoValue(seo)
seo.Description = description
setDoc["seoInfo"] = seo
}
func playletToWebResp(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string, categoryId *int64) *proto.PlayletListResp {
binds := playletmod.EffectiveCategoryBinds(item)
var filterCategoryId int64
if categoryId != nil {
filterCategoryId = *categoryId
}
return &proto.PlayletListResp{
Id: item.Id,
Status: item.Status,
MediaSource: item.MediaSource,
IsFeatured: item.IsFeatured,
IsOriginal: item.IsOriginal,
ShowOriginal: item.ShowOriginal,
MediaBusiness: item.MediaBusiness,
IsRecommend: item.IsRecommend,
ShowOnBanner: item.ShowOnBanner,
Banner: item.Banner,
MobileBanner: item.MobileBanner,
ReviewAt: item.ReviewAt,
OnlineAt: item.OnlineAt,
ThemeIds: item.ThemeIds,
ThemeNames: themeNamesFromMap(item.ThemeIds, themeNameMap),
CategoryIds: playletmod.CategoryIdsFromBinds(binds),
CategoryBinds: toProtoCategoryBinds(binds),
CategoryRank: playletmod.CategoryRankOf(binds, filterCategoryId),
Title: item.Title,
PlayName: item.PlayName,
Description: item.Description,
Slug: item.Slug,
Desc: item.Desc,
Anchor: item.Anchor,
Cover: item.Cover,
HorizontalCover: item.HorizontalCover,
CompressedHorizontalCover: item.CompressedHorizontalCover,
Preview: item.Preview,
TagIds: item.TagIds,
TagNames: tagNamesFromMap(item.TagIds, tagNameMap),
ContentType: item.ContentType,
OperateType: playletmod.NormalizeOperateType(item.OperateType),
PayType: item.PayType,
SingleCoins: item.SingleCoins,
TotalEpisode: item.TotalEpisode,
UpdateStatus: item.UpdateStatus,
TopRank: item.TopRank,
RecommendRank: item.RecommendRank,
CountComment: item.CountComment,
CountBuy: item.CountBuy,
CountBrowse: item.CountBrowse,
HeatValue: item.HeatValue,
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,
CompressedCover: item.CompressedCover,
}
}
func playletDisplayCountLike(item *playletmod.Playlet) int64 {
if item == nil {
return 0
}
return item.CountLike + item.VirtualCountLike
}
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)
if len(themeIds) == 0 {
return result, nil
}
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
})
if err != nil {
return nil, err
}
for _, theme := range themes {
if theme == nil {
continue
}
result[theme.Id] = theme.Name
}
return result, nil
}
// themeNamesFromMap 按题材ID顺序返回题材名称
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)
if len(tagIds) == 0 {
return result, nil
}
tags, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{
playlettagmod.FN.PlayletTag.Id: bson.M{"$in": tagIds},
})
if err != nil {
return nil, err
}
for _, tag := range tags {
if tag == nil {
continue
}
result[tag.Id] = tag.Name
}
return result, nil
}
// tagNamesFromMap 按标签ID顺序返回标签名称
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
}
// normalizePlayletTagIds 校验并去重标签ID
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 chapterToWebResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterListResp {
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,
Status: item.Status,
PayType: item.PayType,
SingleCoins: item.SingleCoins,
WatermarkStatus: item.WatermarkStatus,
MediaSourceId: item.MediaSourceId,
MediaTaskId: item.MediaTaskId,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
func validatePlayletTagIds(ctx context.Context, tagIds []int64) *srvdef.SrvErr {
if len(tagIds) == 0 {
return nil
}
uniqueIds := make([]int64, 0, len(tagIds))
seen := make(map[int64]struct{}, len(tagIds))
for _, tagId := range tagIds {
if tagId <= 0 {
return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
}
if _, ok := seen[tagId]; ok {
continue
}
seen[tagId] = struct{}{}
uniqueIds = append(uniqueIds, tagId)
}
count, err := playlettagmod.CountPlayletTag(ctx, bson.M{
playlettagmod.FN.PlayletTag.Id: bson.M{"$in": uniqueIds},
})
if err != nil {
return err
}
if count != int64(len(uniqueIds)) {
return srvdef.New(srvdef.ErrParamError, "标签不存在", "")
}
return nil
}
// validatePlayletThemeIds 校验题材ID是否存在
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
}