- 移除 买点 (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>
1920 lines
63 KiB
Go
1920 lines
63 KiB
Go
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,
|
||
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.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,
|
||
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
|
||
}
|