2590 lines
96 KiB
Go
2590 lines
96 KiB
Go
package playletser
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"videoweb/app/api/proto"
|
|
"videoweb/app/service/configser"
|
|
"videoweb/app/service/moreser"
|
|
"videoweb/app/service/reportser"
|
|
"videoweb/app/service/slugser"
|
|
"videoweb/app/service/translationser"
|
|
"videoweb/app/service/videoser"
|
|
"videoweb/pub/common/enum/redisconst"
|
|
"videoweb/pub/common/log"
|
|
"videoweb/pub/common/netutil"
|
|
"videoweb/pub/common/response"
|
|
"videoweb/pub/common/srvdef"
|
|
"videoweb/pub/common/useragent"
|
|
"videoweb/pub/common/utils"
|
|
"videoweb/pub/models"
|
|
"videoweb/pub/models/announcementmod"
|
|
"videoweb/pub/models/contenttagmod"
|
|
"videoweb/pub/models/playletchaptermod"
|
|
"videoweb/pub/models/playletfavoritemod"
|
|
"videoweb/pub/models/playlethomeconfigmod"
|
|
"videoweb/pub/models/playletlikemod"
|
|
"videoweb/pub/models/playletmod"
|
|
"videoweb/pub/models/playletthememod"
|
|
"videoweb/pub/models/videocategorymod"
|
|
"videoweb/pub/models/videoinformationmod"
|
|
"videoweb/pub/pubdef"
|
|
"videoweb/pub/redis"
|
|
"videoweb/pub/storage/proxy"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
playletGuessMinPageSize int64 = 8
|
|
playletGuessMaxPageSize int64 = 100
|
|
playletRecommendDefaultPageSize int64 = 20
|
|
playletRecommendMaxPageSize int64 = 100
|
|
playletRecommendAdInterval int64 = 8
|
|
playletListAdInterval int64 = 4
|
|
playletContentPlayletAdInterval int64 = 4
|
|
playletContentVideoAdInterval int64 = 5
|
|
homePlayletListLimit int64 = 12
|
|
playletDetailHotLimit int64 = 11
|
|
)
|
|
|
|
// PlayletGuessList 分页查询猜你喜欢短剧,按观看量降序返回上架短剧
|
|
func PlayletGuessList(ctx context.Context, req *proto.PlayletGuessListReq) (*proto.PlayletGuessListResp, *srvdef.SrvErr) {
|
|
if req == nil || req.Id < 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID无效")
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletGuessMinPageSize, playletGuessMaxPageSize)
|
|
if pageSize < playletGuessMinPageSize {
|
|
pageSize = playletGuessMinPageSize
|
|
}
|
|
filter := bson.M{
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}
|
|
if req.Id > 0 {
|
|
filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": req.Id}
|
|
}
|
|
total, err := playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &proto.PlayletGuessListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: total,
|
|
PageNum: int(pageNum),
|
|
PageSize: int(pageSize),
|
|
List: make([]*proto.PlayletRecommendListItem, 0),
|
|
}}
|
|
if total == 0 || pageNum > (total-1)/pageSize+1 {
|
|
return resp, nil
|
|
}
|
|
list, err := findPlayletRecommendPage(ctx, filter, bson.D{
|
|
{Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}, (pageNum-1)*pageSize, pageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cdn := configser.ImageCdnFromCtx(ctx)
|
|
items := make([]*proto.PlayletRecommendListItem, 0, len(list))
|
|
for _, item := range list {
|
|
if item != nil {
|
|
items = append(items, playletToRecommendListItem(item, cdn, tagNames, themeNames))
|
|
}
|
|
}
|
|
resp.List = items
|
|
return resp, nil
|
|
}
|
|
|
|
// playletRankAggItem 短剧排行榜聚合结果,只保留榜单展示需要的轻量字段
|
|
type playletRankAggItem struct {
|
|
playletmod.Playlet `bson:",inline"`
|
|
RankScore int64 `bson:"rankScore"`
|
|
}
|
|
|
|
type playletRankCount struct {
|
|
Total int64 `bson:"total"`
|
|
}
|
|
|
|
// PlayletThemeList 查询全部已启用短剧题材
|
|
func PlayletThemeList(ctx context.Context) (*proto.PlayletThemeListResp, *srvdef.SrvErr) {
|
|
list, err := playletthememod.FindManyPlayletTheme(ctx,
|
|
bson.M{playletthememod.FN.PlayletTheme.Status: true},
|
|
options.Find().
|
|
SetProjection(bson.D{
|
|
{Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.Slug, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.Icon, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.Rank, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Title, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Description, Value: 1},
|
|
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Keyworld, Value: 1},
|
|
}).
|
|
SetSort(bson.D{
|
|
{Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
|
|
{Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cdn := configser.ImageCdnFromCtx(ctx)
|
|
resp := &proto.PlayletThemeListResp{List: make([]proto.PlayletThemeItem, 0, len(list))}
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
resp.List = append(resp.List, proto.PlayletThemeItem{
|
|
Id: item.Id, Name: item.Name, Slug: item.Slug,
|
|
Icon: netutil.MergeURL(cdn, item.Icon), Rank: item.Rank, SeoInfo: item.SeoInfo,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
var appPlayletRecommendProjection = bson.D{
|
|
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
|
{Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
|
|
{Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
|
|
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
|
{Key: playletmod.FN.Playlet.ShowOriginal, Value: 1},
|
|
{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
|
|
{Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
|
|
{Key: playletmod.FN.Playlet.MediaCreatedAt, Value: 1},
|
|
{Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Slug, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Desc, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Anchor, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
|
{Key: playletmod.FN.Playlet.Preview, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
|
|
{Key: playletmod.FN.Playlet.TagIds, Value: 1},
|
|
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
|
|
{Key: playletmod.FN.Playlet.ContentType, Value: 1},
|
|
{Key: playletmod.FN.Playlet.PayType, Value: 1},
|
|
{Key: playletmod.FN.Playlet.SingleCoins, Value: 1},
|
|
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
|
{Key: playletmod.FN.Playlet.FreeEpisodeCount, Value: 1},
|
|
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
|
|
{Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1},
|
|
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
|
{Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
|
|
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
|
|
{Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1},
|
|
{Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1},
|
|
{Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1},
|
|
{Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
|
|
}
|
|
|
|
// playletDetailProjection 短剧详情投影,额外返回详情页需要的评论数量
|
|
func playletDetailProjection() bson.D {
|
|
projection := make(bson.D, 0, len(appPlayletRecommendProjection)+1)
|
|
projection = append(projection, appPlayletRecommendProjection...)
|
|
projection = append(projection, bson.E{Key: playletmod.FN.Playlet.CountComment, Value: 1})
|
|
return projection
|
|
}
|
|
|
|
// PlayletRecommendList 分页查询 app 端短剧推荐列表,并按固定间隔穿插列表广告
|
|
func PlayletRecommendList(ctx context.Context, req *proto.PlayletRecommendListReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
categoryId, err := slugser.PlayletCategoryID(ctx, req.CategoryId, req.CategorySlug, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagId, err := slugser.ContentTagID(ctx, req.TagId, req.TagSlug, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.CategoryId = categoryId
|
|
req.TagId = tagId
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
|
|
filter, sort := playletRecommendFilterAndSort(req.TagId, req.CategoryId, req.SortList)
|
|
config := configser.GetRequestConfig(ctx)
|
|
ads := configser.GetAdvitise().PostList
|
|
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ads) == 0 {
|
|
return playletRecommendListWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
|
|
}
|
|
adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), playletRecommendAdInterval)
|
|
mixedTotalCount := totalCount + adsTotalCount
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedTotalCount || totalCount == 0 {
|
|
resp := emptyPlayletRecommendListPage(pageNum, pageSize)
|
|
resp.TotalCount = mixedTotalCount
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedTotalCount {
|
|
end = mixedTotalCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, playletRecommendAdInterval)
|
|
list := playletmod.PlayletList{}
|
|
if contentLimit > 0 {
|
|
list, err = findPlayletRecommendPage(ctx, filter, sort, contentStart, contentLimit, req.CategoryId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := buildPlayletRecommendMixedList(list, ads, config.ImgCdn, tagNameMap, themeNameMap, start, end, contentStart, adsTotalCount)
|
|
return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedTotalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}}, nil
|
|
}
|
|
|
|
// PlayletList 分页查询已上架短剧,每四部短剧后穿插一条广告
|
|
func PlayletList(ctx context.Context, req *proto.PlayletListReq) (*proto.PlayletListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
|
}
|
|
if req.SortType != 0 && req.SortType != 1 && req.SortType != 2 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "排序方式无效", "")
|
|
}
|
|
filter := playletListFilter()
|
|
if slug := strings.TrimSpace(req.ThemeSlug); slug != "" || req.ThemeId > 0 {
|
|
themeFilter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
|
|
if slug != "" {
|
|
themeFilter[playletthememod.FN.PlayletTheme.Slug] = slug
|
|
} else {
|
|
themeFilter[playletthememod.FN.PlayletTheme.Id] = req.ThemeId
|
|
}
|
|
theme, err := playletthememod.FindOnePlayletTheme(ctx, themeFilter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter[playletmod.FN.Playlet.ThemeIds] = theme.Id
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
|
|
playletCount, err := playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
advitise := configser.GetAdvitise()
|
|
ads := advitise.PostList
|
|
adCount := playletListInsertedAdsCount(playletCount, len(ads))
|
|
mixedCount := playletCount + adCount
|
|
resp := &proto.PlayletListResp{
|
|
BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
|
|
List: make([]*proto.PlayletRecommendListItem, 0),
|
|
},
|
|
Advitise: advitise,
|
|
}
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedCount {
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedCount {
|
|
end = mixedCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, playletCount, adCount, playletListAdInterval)
|
|
list := make(playletmod.PlayletList, 0)
|
|
if contentLimit > 0 {
|
|
list, err = findPlayletRecommendPage(ctx, filter, playletListSort(req.SortType), contentStart, contentLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.List = buildPlayletListMixedPage(list, ads, configser.GetRequestConfig(ctx).ImgCdn,
|
|
tagNames, themeNames, start, end, contentStart, adCount)
|
|
return resp, nil
|
|
}
|
|
|
|
// ContentList 分页查询上架短剧或视频,短剧每四条、视频每五条穿插一条广告
|
|
func ContentList(ctx context.Context, req *proto.PlayletContentListReq) (*proto.PlayletContentListResp, *srvdef.SrvErr) {
|
|
if req == nil || (req.DataType != 1 && req.DataType != 2) {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "数据类型无效", "")
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
|
|
var (
|
|
contentCount int64
|
|
err *srvdef.SrvErr
|
|
)
|
|
if req.DataType == 1 {
|
|
contentCount, err = playletmod.CountPlaylet(ctx, bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp})
|
|
} else {
|
|
contentCount, err = videoinformationmod.CountVideoInformations(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
})
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
advitise := configser.GetAdvitise()
|
|
ads := advitise.PostList
|
|
interval := playletContentAdInterval(req.DataType)
|
|
adCount := playletContentInsertedAdsCount(contentCount, len(ads), interval)
|
|
mixedCount := contentCount + adCount
|
|
resp := &proto.PlayletContentListResp{
|
|
BasePageResp: response.BasePageResp{
|
|
TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
|
|
List: make([]*proto.PlayletContentListItem, 0),
|
|
},
|
|
Advitise: advitise,
|
|
}
|
|
start := (pageNum - 1) * pageSize
|
|
if start >= mixedCount {
|
|
return resp, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > mixedCount {
|
|
end = mixedCount
|
|
}
|
|
contentStart, contentLimit := moreser.ContentRange(start, end, contentCount, adCount, interval)
|
|
imgCdn := configser.GetRequestConfig(ctx).ImgCdn
|
|
items := make([]*proto.PlayletContentListItem, 0, contentLimit)
|
|
if contentLimit > 0 {
|
|
items, err = findPlayletContentPage(ctx, req.DataType, contentStart, contentLimit, imgCdn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
resp.List = buildPlayletContentMixedPage(items, ads, imgCdn,
|
|
start, end, contentStart, adCount, interval)
|
|
return resp, nil
|
|
}
|
|
|
|
// playletContentAdInterval 根据数据类型返回广告插入间隔
|
|
func playletContentAdInterval(dataType int64) int64 {
|
|
if dataType == 2 {
|
|
return playletContentVideoAdInterval
|
|
}
|
|
return playletContentPlayletAdInterval
|
|
}
|
|
|
|
// playletContentInsertedAdsCount 只在后续仍有内容时插入广告,素材不足时循环使用
|
|
func playletContentInsertedAdsCount(contentCount int64, adsCount int, interval int64) int64 {
|
|
if contentCount <= 1 || adsCount == 0 {
|
|
return 0
|
|
}
|
|
return (contentCount - 1) / interval
|
|
}
|
|
|
|
// findPlayletContentPage 按数据类型查询当前页需要的内容并转换为统一列表项
|
|
func findPlayletContentPage(ctx context.Context, dataType, skip, limit int64, imgCdn string) ([]*proto.PlayletContentListItem, *srvdef.SrvErr) {
|
|
items := make([]*proto.PlayletContentListItem, 0, limit)
|
|
if dataType == 1 {
|
|
list, err := findPlayletRecommendPage(ctx,
|
|
bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}, playletContentPlayletSort(), skip, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range list {
|
|
data := playletToAppData(item, imgCdn, tagNames, themeNames)
|
|
items = append(items, &proto.PlayletContentListItem{Type: 0, Playlet: &data})
|
|
}
|
|
return items, nil
|
|
}
|
|
list := make(videoinformationmod.VideoInformationList, 0)
|
|
if err := videoinformationmod.AggregateVideoInformations(ctx, &list, playletContentVideoPipeline(skip, limit)); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range list {
|
|
data := playletContentVideoInfo(item, imgCdn)
|
|
items = append(items, &proto.PlayletContentListItem{Type: 0, Video: &data})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// playletContentPlayletSort 先按短剧累计浏览数排序,再按热度和 ID 稳定排序
|
|
func playletContentPlayletSort() bson.D {
|
|
return bson.D{
|
|
{Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
|
|
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}
|
|
}
|
|
|
|
// playletContentVideoPipeline 按视频展示观看数分页,展示观看数为真实数与后台配置数之和
|
|
func playletContentVideoPipeline(skip, limit int64) []bson.M {
|
|
return []bson.M{
|
|
{"$match": bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
|
|
{"$addFields": bson.M{"watchScore": bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.WatchTimes, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 0}},
|
|
}}}},
|
|
{"$sort": bson.D{
|
|
{Key: "watchScore", Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
|
}},
|
|
{"$skip": skip},
|
|
{"$limit": limit},
|
|
{"$project": playletContentVideoProjection()},
|
|
}
|
|
}
|
|
|
|
// playletContentVideoProjection 只读取视频卡片需要的字段
|
|
func playletContentVideoProjection() bson.M {
|
|
return bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Slug: 1,
|
|
videoinformationmod.FN.VideoInformation.Keyworld: 1,
|
|
videoinformationmod.FN.VideoInformation.Desc: 1,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
|
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
|
videoinformationmod.FN.VideoInformation.Tags: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
|
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
|
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
|
}
|
|
}
|
|
|
|
// playletContentVideoInfo 将视频模型转换为列表卡片
|
|
func playletContentVideoInfo(item *videoinformationmod.VideoInformation, imgCdn string) proto.VideoInfo {
|
|
tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
|
|
return proto.VideoInfo{ContentData: proto.ContentData{
|
|
Id: item.Id, Title: item.Title, Slug: item.Slug, AddedTime: item.AddedTime,
|
|
CoverImg: netutil.MergeURL(imgCdn, item.CoverImg), Tags: item.Tags, TagsName: tagNames, TagSlugs: tagSlugs,
|
|
WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
|
|
LikeNum: item.Real.Likes + item.Fake.Likes,
|
|
SeoInfo: pubdef.SeoValue{Title: item.Title, Keyworld: append([]string{}, item.Keyworld...), Description: item.Desc},
|
|
}}
|
|
}
|
|
|
|
// buildPlayletContentMixedPage 按全局位置组装内容与广告并保持跨页间隔一致
|
|
func buildPlayletContentMixedPage(content []*proto.PlayletContentListItem, ads []proxy.AdvitiseInfo, imgCdn string,
|
|
start, end, contentStart, adCount, interval int64) []*proto.PlayletContentListItem {
|
|
items := make([]*proto.PlayletContentListItem, 0, end-start)
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, interval)
|
|
if adIndex >= 0 && len(ads) > 0 {
|
|
ad := ads[adIndex%int64(len(ads))]
|
|
ad.Images = netutil.MergeURL(imgCdn, ad.Images)
|
|
items = append(items, &proto.PlayletContentListItem{Type: 2, Advertise: &ad})
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, interval) - contentStart
|
|
if contentIndex >= 0 && contentIndex < int64(len(content)) {
|
|
items = append(items, content[contentIndex])
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
// playletListFilter 只查询已上架短剧
|
|
func playletListFilter() bson.M {
|
|
return bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
|
|
}
|
|
|
|
// playletListInsertedAdsCount 只在仍有后续短剧时插入广告,广告素材不足时循环使用
|
|
func playletListInsertedAdsCount(playletCount int64, adsCount int) int64 {
|
|
if playletCount <= 1 || adsCount == 0 {
|
|
return 0
|
|
}
|
|
return (playletCount - 1) / playletListAdInterval
|
|
}
|
|
|
|
// playletListSort 根据列表排序方式生成稳定的短剧顺序
|
|
func playletListSort(sortType int64) bson.D {
|
|
switch sortType {
|
|
case 2:
|
|
return bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
default:
|
|
return bson.D{{Key: playletmod.FN.Playlet.HeatValue, Value: -1}, {Key: playletmod.FN.Playlet.CountBrowse, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
}
|
|
}
|
|
|
|
// buildPlayletListMixedPage 根据全局位置组装当前页,避免跨页后重新计算广告间隔
|
|
func buildPlayletListMixedPage(list playletmod.PlayletList, ads []proxy.AdvitiseInfo, imgCdn string,
|
|
tagNames contenttagmod.ContentTagMap, themeNames map[int64]string, start, end, contentStart, adCount int64) []*proto.PlayletRecommendListItem {
|
|
items := make([]*proto.PlayletRecommendListItem, 0, end-start)
|
|
contentByIndex := make(map[int64]*playletmod.Playlet, len(list))
|
|
for i, item := range list {
|
|
contentByIndex[contentStart+int64(i)] = item
|
|
}
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, playletListAdInterval)
|
|
if adIndex >= 0 && len(ads) > 0 {
|
|
items = append(items, playletAdToRecommendListItem(ads[adIndex%int64(len(ads))], imgCdn))
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, playletListAdInterval)
|
|
if item := contentByIndex[contentIndex]; item != nil {
|
|
items = append(items, playletToRecommendListItem(item, imgCdn, tagNames, themeNames))
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
// PlayletTagList 根据统一标签ID分页查询已上架短剧
|
|
func PlayletTagList(ctx context.Context, req *proto.ContentTagPageReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
|
}
|
|
tagId, err := slugser.ContentTagID(ctx, req.Id, req.Slug, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Id = tagId
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
|
|
ok, err := configser.HasEnabledContentTagBind(ctx, req.Id, contenttagmod.ContentTagTypePlaylet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return emptyPlayletRecommendListPage(pageNum, pageSize), nil
|
|
}
|
|
filter := bson.M{
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
playletmod.FN.Playlet.TagIds: req.Id,
|
|
}
|
|
sort := bson.D{
|
|
{Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return playletRecommendListWithoutAds(ctx, filter, sort, 0, pageNum, pageSize, totalCount, config.ImgCdn)
|
|
}
|
|
|
|
// QueryPlayletInfo 查询短剧详情、剧集列表和点赞状态
|
|
func QueryPlayletInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryPlayletInfoReq) (*proto.QueryPlayletInfoResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
playletId, err := slugser.PlayletID(ctx, req.Id, req.Slug, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Id = playletId
|
|
config := configser.GetRequestConfig(ctx)
|
|
playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: req.Id,
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}, options.FindOne().SetProjection(playletDetailProjection()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go incrementPlayletWatchCount(playletInfo)
|
|
chapterList, isLike, err := queryPlayletDetailExtraData(ctx, userId, playletInfo, ip, ua)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isFavorite, err := queryPlayletIsFavorite(ctx, userId, playletInfo.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, playletmod.PlayletList{playletInfo})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := applyOfficialChapterAccess(ctx, playletInfo, chapterList, ip); err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.QueryPlayletInfoResp{
|
|
FreeEpisodeCount: playletInfo.EffectiveFreeEpisodeCount(),
|
|
Path: playletRequestPath(ctx),
|
|
Translations: translationser.GetTranslationData(),
|
|
Config: config,
|
|
BottonTranslations: translationser.GetTranslationBottomList(),
|
|
Advitise: configser.GetAdvitise(),
|
|
Announcement: configser.GetAnnouncement(),
|
|
PlayletInfo: playletDetailInfoData(playletInfo, config.ImgCdn, tagNameMap, themeNameMap, isLike, isFavorite),
|
|
ChapterList: chapterList,
|
|
IsLike: isLike,
|
|
IsFavorite: isFavorite,
|
|
}, nil
|
|
}
|
|
|
|
// QueryPlayletChapterInfo 查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态
|
|
func QueryPlayletChapterInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryPlayletChapterInfoReq) (*proto.QueryPlayletChapterInfoResp, *srvdef.SrvErr) {
|
|
if req == nil || req.Id <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
config := configser.GetRequestConfig(ctx)
|
|
chapterInfo, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: req.Id,
|
|
}, options.FindOne().SetProjection(playletChapterProjection()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: chapterInfo.PlayletId,
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}, options.FindOne().SetProjection(appPlayletRecommendProjection))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !playletInfo.IsOfficialEpisodeOpen(chapterInfo.CurrentEpisode) {
|
|
return nil, srvdef.New(srvdef.ErrNoData, "本集暂未在官网开放", "")
|
|
}
|
|
if err = ensureOriginalChapterWatermarkReady(playletInfo, chapterInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
go incrementPlayletWatchCount(playletInfo)
|
|
go incrementPlayletChapterWatchCount(chapterInfo.Id)
|
|
|
|
hotPlayletList, chapterList, isLike, err := queryPlayletChapterDetailExtraData(ctx, userId, playletInfo, ip, ua)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isFavorite, err := queryPlayletIsFavorite(ctx, userId, playletInfo.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allList := make(playletmod.PlayletList, 0, len(hotPlayletList)+1)
|
|
allList = append(allList, playletInfo)
|
|
allList = append(allList, hotPlayletList...)
|
|
tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, allList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chapterData := make([]proto.PlayletChapterInfo, 0, len(chapterList)+1)
|
|
chapterData = append(chapterData, chapterList...)
|
|
chapterData = append(chapterData, playletChapterToInfo(chapterInfo, ip, ua, isOriginalPlaylet(playletInfo)))
|
|
if err := applyOfficialChapterAccess(ctx, playletInfo, chapterData, ip); err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.QueryPlayletChapterInfoResp{
|
|
FreeEpisodeCount: playletInfo.EffectiveFreeEpisodeCount(),
|
|
Path: playletRequestPath(ctx),
|
|
Translations: translationser.GetTranslationData(),
|
|
Config: config,
|
|
BottonTranslations: translationser.GetTranslationBottomList(),
|
|
Advitise: configser.GetAdvitise(),
|
|
Announcement: configser.GetAnnouncement(),
|
|
ChapterInfo: chapterData[len(chapterData)-1],
|
|
PlayletInfo: playletToAppData(playletInfo, config.ImgCdn, tagNameMap, themeNameMap),
|
|
ChapterList: chapterData[:len(chapterData)-1],
|
|
HotPlayletList: playletListToAppData(hotPlayletList, config.ImgCdn, tagNameMap, themeNameMap),
|
|
IsLike: isLike,
|
|
IsFavorite: isFavorite,
|
|
}, nil
|
|
}
|
|
|
|
func applyOfficialChapterAccess(ctx context.Context, playlet *playletmod.Playlet, chapters []proto.PlayletChapterInfo, ip string) *srvdef.SrvErr {
|
|
if len(chapters) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]int64, 0, len(chapters))
|
|
for i := range chapters {
|
|
ids = append(ids, chapters[i].Id)
|
|
}
|
|
raw, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids},
|
|
playletchaptermod.FN.PlayletChapter.PlayletId: playlet.Id,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
byID := make(map[int64]*playletchaptermod.PlayletChapter, len(raw))
|
|
for _, chapter := range raw {
|
|
byID[chapter.Id] = chapter
|
|
}
|
|
for i := range chapters {
|
|
chapter := byID[chapters[i].Id]
|
|
open := chapter != nil && playlet.IsOfficialEpisodeOpen(chapter.CurrentEpisode)
|
|
chapters[i].IsOfficialOpen = &open
|
|
chapters[i].VideoUrl = ""
|
|
if !open {
|
|
continue
|
|
}
|
|
url, useNewKey := chapterPlaySource(chapter, isOriginalPlaylet(playlet))
|
|
if url != "" {
|
|
chapters[i].VideoUrl = videoser.GetM3U8OfficialParam(chapter.Id, playlet.Id, url, ip, useNewKey)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LikePlaylet 当前登录用户点赞或取消点赞指定短剧
|
|
func LikePlaylet(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.LikePlayletReq) (*proto.LikePlayletResp, *srvdef.SrvErr) {
|
|
if req == nil || userId <= 0 || req.Id <= 0 || req.IsLike == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: req.Id,
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}, options.FindOne().SetProjection(bson.M{
|
|
playletmod.FN.Playlet.Id: 1,
|
|
playletmod.FN.Playlet.Title: 1,
|
|
playletmod.FN.Playlet.Slug: 1,
|
|
playletmod.FN.Playlet.ThemeIds: 1,
|
|
playletmod.FN.Playlet.TagIds: 1,
|
|
playletmod.FN.Playlet.TotalEpisode: 1,
|
|
playletmod.FN.Playlet.CountLike: 1,
|
|
playletmod.FN.Playlet.VirtualCountLike: 1,
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if *req.IsLike {
|
|
created, err := playletlikemod.UpsertOnePlayletLike(ctx, userId, req.Id, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if created {
|
|
playletInfo.CountLike++
|
|
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountLike: 1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := reportser.ReportPlayletLike(userId, ua, ip, playletInfo, 1); err != nil {
|
|
log.Warn("ReportPlayletLike failed", log.Any("playletId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
} else {
|
|
deleted, err := playletlikemod.DeleteOnePlayletLike(ctx, bson.M{
|
|
playletlikemod.FN.PlayletLike.UserID: userId,
|
|
playletlikemod.FN.PlayletLike.PlayletID: req.Id,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deleted {
|
|
if playletInfo.CountLike > 0 {
|
|
playletInfo.CountLike--
|
|
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountLike: -1}}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := reportser.ReportPlayletLike(userId, ua, ip, playletInfo, 2); err != nil {
|
|
log.Warn("ReportPlayletLike failed", log.Any("playletId", req.Id), log.Any("userId", userId), log.E(err))
|
|
}
|
|
}
|
|
}
|
|
return &proto.LikePlayletResp{
|
|
Id: req.Id,
|
|
IsLike: *req.IsLike,
|
|
LikeNum: playletInfo.CountLike,
|
|
}, nil
|
|
}
|
|
|
|
// QueryPlayletRank 分页查询短剧排行榜,读取独立的 10 分钟内存缓存。
|
|
// 点赞榜按累计点赞量、周/月/年榜按对应周期观看量取前 50;不足时随机补位。分页 pageSize 不是 50 时强制为 50。
|
|
func QueryPlayletRank(ctx context.Context, req *proto.PlayletRankReq) (*proto.PlayletRankResp, *srvdef.SrvErr) {
|
|
if req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
period := proto.PlayletRankPeriod(req.RankPeriod)
|
|
if period < proto.PlayletRankPeriodLike || period > proto.PlayletRankPeriodYear {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
|
}
|
|
pageNum, pageSize := configser.NormalizeRankPage(req.PageNum, req.PageSize)
|
|
all := configser.GetPlayletRankList(configser.RankPeriod(period), 0)
|
|
items := configser.SliceRankPage(all, pageNum, pageSize)
|
|
return &proto.PlayletRankResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: int64(len(all)),
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
|
|
}
|
|
|
|
// HomePlayletData 查询首页推荐、最新、最热三组短剧数据
|
|
func HomePlayletData(ctx context.Context, imgCdn string) (proto.HomePlayletDataResp, *srvdef.SrvErr) {
|
|
resp := proto.HomePlayletDataResp{
|
|
RecommendList: make([]proto.PlayletData, 0),
|
|
NewestList: make([]proto.PlayletData, 0),
|
|
HotList: make([]proto.PlayletData, 0),
|
|
}
|
|
var (
|
|
recommendList playletmod.PlayletList
|
|
newestList playletmod.PlayletList
|
|
hotList playletmod.PlayletList
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
err1 *srvdef.SrvErr
|
|
)
|
|
setErr := func(err *srvdef.SrvErr) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if err1 == nil {
|
|
err1 = err
|
|
}
|
|
}
|
|
queryList := func(sortType proto.PlayletRecommendSort, target *playletmod.PlayletList) {
|
|
defer wg.Done()
|
|
filter, sort := playletRecommendFilterAndSort(0, 0, int64(sortType))
|
|
list, err := findPlayletRecommendPage(ctx, filter, sort, 0, homePlayletListLimit)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
*target = list
|
|
}
|
|
wg.Add(3)
|
|
go queryList(proto.PlayletRecommendSortRecommend, &recommendList)
|
|
go queryList(proto.PlayletRecommendSortNewest, &newestList)
|
|
go queryList(proto.PlayletRecommendSortHot, &hotList)
|
|
wg.Wait()
|
|
if err1 != nil {
|
|
return resp, err1
|
|
}
|
|
allList := make(playletmod.PlayletList, 0, len(recommendList)+len(newestList)+len(hotList))
|
|
allList = append(allList, recommendList...)
|
|
allList = append(allList, newestList...)
|
|
allList = append(allList, hotList...)
|
|
tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, allList)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
resp.RecommendList = playletListToAppData(recommendList, imgCdn, tagNameMap, themeNameMap)
|
|
resp.NewestList = playletListToAppData(newestList, imgCdn, tagNameMap, themeNameMap)
|
|
resp.HotList = playletListToAppData(hotList, imgCdn, tagNameMap, themeNameMap)
|
|
return resp, nil
|
|
}
|
|
|
|
// playletRecommendFilterAndSort 根据排序枚举构建短剧查询条件和排序规则
|
|
func playletRecommendFilterAndSort(tagId, categoryId, sortList int64) (bson.M, bson.D) {
|
|
filter := bson.M{
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}
|
|
if tagId > 0 {
|
|
filter[playletmod.FN.Playlet.TagIds] = tagId
|
|
}
|
|
if categoryId > 0 {
|
|
filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{categoryId}}
|
|
}
|
|
switch proto.PlayletRecommendSort(sortList) {
|
|
case proto.PlayletRecommendSortNewest:
|
|
return filter, bson.D{
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}
|
|
case proto.PlayletRecommendSortHot:
|
|
return filter, bson.D{
|
|
{Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}
|
|
default:
|
|
if categoryId <= 0 {
|
|
filter[playletmod.FN.Playlet.IsRecommend] = true
|
|
} else {
|
|
return filter, playletmod.CategoryRankSort()
|
|
}
|
|
return filter, bson.D{
|
|
{Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
|
|
{Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}
|
|
}
|
|
}
|
|
|
|
// findPlayletRecommendPage 查询当前混排分页需要的短剧数据
|
|
func findPlayletRecommendPage(ctx context.Context, filter bson.M, sort bson.D, skip, limit int64, categoryIds ...int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
|
if len(categoryIds) > 0 && categoryIds[0] > 0 && len(sort) > 0 && sort[0].Key == "_categoryRank" {
|
|
return playletmod.FindManyPlayletByCategoryRank(ctx, filter, categoryIds[0], skip, limit, sort, appPlayletRecommendProjection)
|
|
}
|
|
computedSort := make(bson.D, len(sort))
|
|
copy(computedSort, sort)
|
|
addFields := bson.M{}
|
|
for i := range computedSort {
|
|
switch computedSort[i].Key {
|
|
case playletmod.FN.Playlet.CountBrowse:
|
|
computedSort[i].Key = "displayCountBrowse"
|
|
addFields["displayCountBrowse"] = bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.CountBrowse, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualCountBrowse, 0}},
|
|
}}
|
|
case playletmod.FN.Playlet.HeatValue:
|
|
computedSort[i].Key = "displayHeatValue"
|
|
addFields["displayHeatValue"] = bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.HeatValue, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualHeatValue, 0}},
|
|
}}
|
|
}
|
|
}
|
|
if len(addFields) > 0 {
|
|
list := make(playletmod.PlayletList, 0)
|
|
pipeline := []bson.M{
|
|
{"$match": filter},
|
|
{"$addFields": addFields},
|
|
{"$sort": computedSort},
|
|
{"$skip": skip},
|
|
{"$limit": limit},
|
|
{"$project": appPlayletRecommendProjection},
|
|
}
|
|
if err := playletmod.AggregatePlaylet(ctx, &list, pipeline, options.Aggregate().SetAllowDiskUse(true)); err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
return playletmod.FindManyPlaylet(ctx, filter, options.Find().
|
|
SetProjection(appPlayletRecommendProjection).
|
|
SetSort(sort).
|
|
SetSkip(skip).
|
|
SetLimit(limit))
|
|
}
|
|
|
|
// playletListToAppData 批量转换短剧模型为 app 短剧数据
|
|
func playletListToAppData(list playletmod.PlayletList, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) []proto.PlayletData {
|
|
result := make([]proto.PlayletData, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
result = append(result, playletToAppData(item, imgCdn, tagNameMap, themeNameMap))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// playletRecommendListWithoutAds 无广告时直接按短剧分页查询
|
|
func playletRecommendListWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, imgCdn string) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
|
|
list, err := findPlayletRecommendPage(ctx, filter, sort, (pageNum-1)*pageSize, pageSize, categoryId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]*proto.PlayletRecommendListItem, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
items = append(items, playletToRecommendListItem(item, imgCdn, tagNameMap, themeNameMap))
|
|
}
|
|
return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: totalCount,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: items,
|
|
}}, nil
|
|
}
|
|
|
|
// buildPlayletRecommendMixedList 按混排后的全局位置切出当前页短剧和广告数据
|
|
func buildPlayletRecommendMixedList(list playletmod.PlayletList, ads []proxy.AdvitiseInfo, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, start, end, contentStart, adsTotal int64) []*proto.PlayletRecommendListItem {
|
|
items := make([]*proto.PlayletRecommendListItem, 0, end-start)
|
|
contentByIndex := make(map[int64]*playletmod.Playlet, len(list))
|
|
for i, item := range list {
|
|
contentByIndex[contentStart+int64(i)] = item
|
|
}
|
|
for pos := start; pos < end; pos++ {
|
|
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, playletRecommendAdInterval)
|
|
if adIndex >= 0 && adIndex < adsTotal {
|
|
items = append(items, playletAdToRecommendListItem(ads[adIndex], imgCdn))
|
|
continue
|
|
}
|
|
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, playletRecommendAdInterval)
|
|
if item := contentByIndex[contentIndex]; item != nil {
|
|
items = append(items, playletToRecommendListItem(item, imgCdn, tagNameMap, themeNameMap))
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
// playletToRecommendListItem 将短剧模型转换为 app 推荐短剧列表项
|
|
func playletToRecommendListItem(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) *proto.PlayletRecommendListItem {
|
|
return &proto.PlayletRecommendListItem{
|
|
Type: proto.PlayletRecommendItemTypePlaylet,
|
|
PlayletData: playletToAppData(item, imgCdn, tagNameMap, themeNameMap),
|
|
}
|
|
}
|
|
|
|
// playletAdToRecommendListItem 将广告转换为 app 推荐短剧列表项
|
|
func playletAdToRecommendListItem(ad proxy.AdvitiseInfo, imgCdn string) *proto.PlayletRecommendListItem {
|
|
return &proto.PlayletRecommendListItem{
|
|
Type: proto.PlayletRecommendItemTypeAdvertise,
|
|
AdvertiseId: ad.Id,
|
|
Link: ad.Link,
|
|
Location: ad.Location,
|
|
PlayletData: proto.PlayletData{
|
|
Title: ad.Title,
|
|
Cover: netutil.MergeURL(imgCdn, ad.Images),
|
|
},
|
|
}
|
|
}
|
|
|
|
// queryPlayletDetailExtraData 并发查询详情页附加数据,缩短详情接口整体等待时间
|
|
func queryPlayletDetailExtraData(ctx context.Context, userId int64, playletInfo *playletmod.Playlet, ip, ua string) ([]proto.PlayletChapterInfo, bool, *srvdef.SrvErr) {
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var firstErr *srvdef.SrvErr
|
|
chapterList := make([]proto.PlayletChapterInfo, 0)
|
|
isLike := false
|
|
setErr := func(err *srvdef.SrvErr) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryPlayletChapterList(ctx, playletInfo.Id, ip, ua, isOriginalPlaylet(playletInfo))
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
chapterList = data
|
|
mu.Unlock()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryPlayletIsLike(ctx, userId, playletInfo.Id)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
isLike = data
|
|
mu.Unlock()
|
|
}()
|
|
wg.Wait()
|
|
if firstErr != nil {
|
|
return nil, false, firstErr
|
|
}
|
|
return chapterList, isLike, nil
|
|
}
|
|
|
|
// queryPlayletChapterDetailExtraData 并发查询剧集详情页附加数据,热门短剧只按相同分类查询且不做兜底
|
|
func queryPlayletChapterDetailExtraData(ctx context.Context, userId int64, playletInfo *playletmod.Playlet, ip, ua string) (playletmod.PlayletList, []proto.PlayletChapterInfo, bool, *srvdef.SrvErr) {
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var firstErr *srvdef.SrvErr
|
|
hotPlayletList := make(playletmod.PlayletList, 0)
|
|
chapterList := make([]proto.PlayletChapterInfo, 0)
|
|
isLike := false
|
|
setErr := func(err *srvdef.SrvErr) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
wg.Add(3)
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryPlayletChapterDetailHotList(ctx, playletInfo)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
hotPlayletList = data
|
|
mu.Unlock()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryPlayletChapterList(ctx, playletInfo.Id, ip, ua, isOriginalPlaylet(playletInfo))
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
chapterList = data
|
|
mu.Unlock()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := queryPlayletIsLike(ctx, userId, playletInfo.Id)
|
|
if err != nil {
|
|
setErr(err)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
isLike = data
|
|
mu.Unlock()
|
|
}()
|
|
wg.Wait()
|
|
if firstErr != nil {
|
|
return nil, nil, false, firstErr
|
|
}
|
|
return hotPlayletList, chapterList, isLike, nil
|
|
}
|
|
|
|
// incrementPlayletWatchCount 异步增加短剧浏览、热度和周/月/年窗口观看数。
|
|
// 周/月/年窗口按北京时间切:周一 00:00、每月 1 日 00:00、每年 1 月 1 日 00:00。
|
|
func incrementPlayletWatchCount(playletInfo *playletmod.Playlet) {
|
|
if playletInfo == nil {
|
|
return
|
|
}
|
|
now := utils.Now()
|
|
incDoc := bson.M{
|
|
playletmod.FN.Playlet.CountBrowse: 1,
|
|
playletmod.FN.Playlet.HeatValue: 1,
|
|
}
|
|
setDoc := bson.M{
|
|
playletmod.FN.Playlet.WatchUpdatedAt: now,
|
|
}
|
|
if playletInfo.WatchUpdatedAt.After(utils.BeginOfWeekByTime(now)) {
|
|
incDoc[playletmod.FN.Playlet.WeekWatchs] = 1
|
|
} else {
|
|
setDoc[playletmod.FN.Playlet.WeekWatchs] = 1
|
|
}
|
|
if playletInfo.WatchUpdatedAt.After(utils.BeginOfMonth(now)) {
|
|
incDoc[playletmod.FN.Playlet.MonthWatchs] = 1
|
|
} else {
|
|
setDoc[playletmod.FN.Playlet.MonthWatchs] = 1
|
|
}
|
|
if playletInfo.WatchUpdatedAt.After(utils.BeginOfYear(now)) {
|
|
incDoc[playletmod.FN.Playlet.YearWatchs] = 1
|
|
} else {
|
|
setDoc[playletmod.FN.Playlet.YearWatchs] = 1
|
|
}
|
|
if err := playletmod.UpdateOnePlaylet(context.Background(), bson.M{
|
|
playletmod.FN.Playlet.Id: playletInfo.Id,
|
|
}, bson.M{"$set": setDoc, "$inc": incDoc}); err != nil {
|
|
log.Warn("incrementPlayletWatchCount failed", log.Any("playletId", playletInfo.Id), log.E(err))
|
|
}
|
|
}
|
|
|
|
// incrementPlayletChapterWatchCount 异步增加短剧剧集浏览数,避免阻塞剧集详情读取
|
|
func incrementPlayletChapterWatchCount(chapterId int64) {
|
|
if chapterId <= 0 {
|
|
return
|
|
}
|
|
if err := playletchaptermod.UpdateOnePlayletChapter(context.Background(), bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: chapterId,
|
|
}, bson.M{"$inc": bson.M{
|
|
playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
|
|
}}); err != nil {
|
|
log.Warn("更新短剧剧集浏览数失败", log.Any("chapterId", chapterId), log.E(err))
|
|
}
|
|
}
|
|
|
|
// queryPlayletChapterDetailHotList 查询剧集详情页热门短剧,按相同分类、累计点击量、更新时间排序,最多返回11条
|
|
func queryPlayletChapterDetailHotList(ctx context.Context, playletInfo *playletmod.Playlet) (playletmod.PlayletList, *srvdef.SrvErr) {
|
|
filter := bson.M{
|
|
playletmod.FN.Playlet.Id: bson.M{"$ne": playletInfo.Id},
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}
|
|
if len(playletInfo.CategoryIds) > 0 {
|
|
filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": playletInfo.CategoryIds}
|
|
}
|
|
return findPlayletRecommendPage(ctx, filter, bson.D{
|
|
{Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}, 0, playletDetailHotLimit)
|
|
}
|
|
|
|
// queryPlayletChapterList 查询短剧剧集列表,并按集数升序返回。
|
|
// 原创短剧只返回已有水印地址的剧集:首次未处理好的不露出,换水印期间仍可播旧地址。
|
|
func queryPlayletChapterList(ctx context.Context, playletId int64, ip, ua string, isOriginal bool) ([]proto.PlayletChapterInfo, *srvdef.SrvErr) {
|
|
filter := bson.M{
|
|
playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
|
|
}
|
|
if isOriginal {
|
|
// 必须含非空白水印地址;空串、空白、缺字段都排除
|
|
filter[playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl] = bson.M{"$regex": "\\S"}
|
|
}
|
|
list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find().
|
|
SetProjection(playletChapterProjection()).
|
|
SetSort(bson.D{
|
|
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
|
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data := make([]proto.PlayletChapterInfo, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
if isOriginal && strings.TrimSpace(item.WatermarkVideoUrl) == "" {
|
|
continue
|
|
}
|
|
data = append(data, playletChapterToInfo(item, ip, ua, isOriginal))
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// playletChapterProjection 限定详情页剧集字段,避免返回整条剧集文档
|
|
func playletChapterProjection() bson.M {
|
|
return bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: 1,
|
|
playletchaptermod.FN.PlayletChapter.PlayletId: 1,
|
|
playletchaptermod.FN.PlayletChapter.MediaContentId: 1,
|
|
playletchaptermod.FN.PlayletChapter.CurrentEpisode: 1,
|
|
playletchaptermod.FN.PlayletChapter.Title: 1,
|
|
playletchaptermod.FN.PlayletChapter.Md5: 1,
|
|
playletchaptermod.FN.PlayletChapter.Hash: 1,
|
|
playletchaptermod.FN.PlayletChapter.Height: 1,
|
|
playletchaptermod.FN.PlayletChapter.Width: 1,
|
|
playletchaptermod.FN.PlayletChapter.Ratio: 1,
|
|
playletchaptermod.FN.PlayletChapter.VideoUrl: 1,
|
|
playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl: 1,
|
|
playletchaptermod.FN.PlayletChapter.MediaSize: 1,
|
|
playletchaptermod.FN.PlayletChapter.MediaTime: 1,
|
|
playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
|
|
playletchaptermod.FN.PlayletChapter.CountCollect: 1,
|
|
playletchaptermod.FN.PlayletChapter.CountLike: 1,
|
|
playletchaptermod.FN.PlayletChapter.CountComment: 1,
|
|
playletchaptermod.FN.PlayletChapter.PayType: 1,
|
|
playletchaptermod.FN.PlayletChapter.SingleCoins: 1,
|
|
playletchaptermod.FN.PlayletChapter.WatermarkStatus: 1,
|
|
playletchaptermod.FN.PlayletChapter.CreatedAt: 1,
|
|
playletchaptermod.FN.PlayletChapter.UpdatedAt: 1,
|
|
}
|
|
}
|
|
|
|
// playletChapterToInfo 将短剧剧集模型转换为详情页剧集数据。
|
|
// 原创短剧只下水印地址,避免未处理好的剧集回退成原片地址。
|
|
func playletChapterToInfo(item *playletchaptermod.PlayletChapter, ip, ua string, isOriginal bool) proto.PlayletChapterInfo {
|
|
_ = ua
|
|
videoURL, useNewKey := chapterPlaySource(item, isOriginal)
|
|
if videoURL != "" {
|
|
videoURL = videoser.GetPlayletM3U8AuthParam(item.Id, videoURL, ip, useNewKey)
|
|
}
|
|
return proto.PlayletChapterInfo{
|
|
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: videoURL,
|
|
MediaSize: item.MediaSize,
|
|
MediaTime: item.MediaTime,
|
|
CountBrowse: item.CountBrowse,
|
|
CountCollect: item.CountCollect,
|
|
CountLike: item.CountLike,
|
|
CountComment: item.CountComment,
|
|
PayType: item.PayType,
|
|
SingleCoins: item.SingleCoins,
|
|
WatermarkStatus: item.WatermarkStatus,
|
|
CreatedAt: item.CreatedAt,
|
|
UpdatedAt: item.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// chapterPlaySource 优先用水印地址。没有水印时,非原创短剧回退原片,并标记使用原片密钥。
|
|
func chapterPlaySource(chapter *playletchaptermod.PlayletChapter, isOriginal bool) (string, bool) {
|
|
if chapter == nil {
|
|
return "", false
|
|
}
|
|
if url := strings.TrimSpace(chapter.WatermarkVideoUrl); url != "" {
|
|
return url, false
|
|
}
|
|
if isOriginal {
|
|
return "", false
|
|
}
|
|
url := strings.TrimSpace(chapter.VideoUrl)
|
|
return url, url != ""
|
|
}
|
|
|
|
// isOriginalPlaylet 判断是否原创短剧。
|
|
// 后台新增剧没有媒资ID;历史数据可能缺 isOriginal 字段,按无媒资ID兜底。
|
|
func isOriginalPlaylet(playlet *playletmod.Playlet) bool {
|
|
if playlet == nil {
|
|
return false
|
|
}
|
|
return playlet.IsOriginal || playlet.MediaCenterId == 0
|
|
}
|
|
|
|
// ensureOriginalChapterWatermarkReady 原创剧集必须已有水印地址才允许播放。
|
|
func ensureOriginalChapterWatermarkReady(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *srvdef.SrvErr {
|
|
if !isOriginalPlaylet(playlet) || chapter == nil {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(chapter.WatermarkVideoUrl) != "" {
|
|
return nil
|
|
}
|
|
return srvdef.New(srvdef.ErrNoData, "剧集处理中,请稍后观看", "剧集处理中,请稍后观看")
|
|
}
|
|
|
|
// queryPlayletIsLike 判断当前登录用户是否已点赞该短剧,未登录直接返回 false
|
|
func queryPlayletIsLike(ctx context.Context, userId, playletId int64) (bool, *srvdef.SrvErr) {
|
|
if userId <= 0 {
|
|
return false, nil
|
|
}
|
|
count, err := playletlikemod.CountPlayletLike(ctx, bson.M{
|
|
playletlikemod.FN.PlayletLike.UserID: userId,
|
|
playletlikemod.FN.PlayletLike.PlayletID: playletId,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// playletDetailInfoData 将短剧模型转换为详情页专用响应结构
|
|
func playletDetailInfoData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, isLike, isFavorite bool) proto.PlayletDetailInfo {
|
|
return proto.PlayletDetailInfo{
|
|
PlayletData: playletToAppData(item, imgCdn, tagNameMap, themeNameMap),
|
|
CountComment: item.CountComment,
|
|
IsLike: isLike,
|
|
IsFavorite: isFavorite,
|
|
}
|
|
}
|
|
|
|
// queryPlayletIsFavorite 判断当前用户是否已收藏短剧,未登录返回 false
|
|
func queryPlayletIsFavorite(ctx context.Context, userID, playletID int64) (bool, *srvdef.SrvErr) {
|
|
if userID <= 0 {
|
|
return false, nil
|
|
}
|
|
count, err := playletfavoritemod.Count(ctx, bson.M{playletfavoritemod.FieldUserID: userID, playletfavoritemod.FieldPlayletID: playletID})
|
|
return count > 0, err
|
|
}
|
|
|
|
// countPlayletTotalLikeRank 统计累计点赞榜可展示短剧数量
|
|
func countPlayletTotalLikeRank(ctx context.Context) (int64, *srvdef.SrvErr) {
|
|
rows := make([]playletRankCount, 0)
|
|
if err := playletmod.AggregatePlaylet(ctx, &rows, playletTotalLikeRankCountPipeline()); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return 0, nil
|
|
}
|
|
return rows[0].Total, nil
|
|
}
|
|
|
|
// aggregatePlayletTotalLikeRank 分页查询累计点赞榜短剧
|
|
func aggregatePlayletTotalLikeRank(ctx context.Context, skip, limit int64) ([]playletRankAggItem, *srvdef.SrvErr) {
|
|
list := make([]playletRankAggItem, 0)
|
|
if err := playletmod.AggregatePlaylet(ctx, &list, playletTotalLikeRankPagePipeline(skip, limit)); err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// countPlayletPeriodLikeRank 统计周期新增点赞榜可展示短剧数量
|
|
func countPlayletPeriodLikeRank(ctx context.Context, since time.Time) (int64, *srvdef.SrvErr) {
|
|
rows := make([]playletRankCount, 0)
|
|
if err := playletlikemod.AggregatePlayletLike(ctx, &rows, playletPeriodLikeRankCountPipeline(since)); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return 0, nil
|
|
}
|
|
return rows[0].Total, nil
|
|
}
|
|
|
|
// aggregatePlayletPeriodLikeRank 分页查询周期新增点赞榜短剧
|
|
func aggregatePlayletPeriodLikeRank(ctx context.Context, since time.Time, skip, limit int64) ([]playletRankAggItem, *srvdef.SrvErr) {
|
|
list := make([]playletRankAggItem, 0)
|
|
if err := playletlikemod.AggregatePlayletLike(ctx, &list, playletPeriodLikeRankPagePipeline(since, skip, limit)); err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// playletRankPeriodSince 将排行榜周期转换为点赞起始时间
|
|
func playletRankPeriodSince(period proto.PlayletRankPeriod) time.Time {
|
|
now := time.Now()
|
|
switch period {
|
|
case proto.PlayletRankPeriodWeek:
|
|
return now.AddDate(0, 0, -7)
|
|
case proto.PlayletRankPeriodMonth:
|
|
return now.AddDate(0, -1, 0)
|
|
case proto.PlayletRankPeriodYear:
|
|
return now.AddDate(-1, 0, 0)
|
|
default:
|
|
return time.Time{}
|
|
}
|
|
}
|
|
|
|
// playletTotalLikeRankCountPipeline 构建累计点赞榜数量统计管道
|
|
func playletTotalLikeRankCountPipeline() []bson.M {
|
|
return append(playletTotalLikeRankBasePipeline(), bson.M{"$count": "total"})
|
|
}
|
|
|
|
// playletTotalLikeRankPagePipeline 构建累计点赞榜分页查询管道
|
|
func playletTotalLikeRankPagePipeline(skip, limit int64) []bson.M {
|
|
pipeline := playletTotalLikeRankBasePipeline()
|
|
pipeline = append(pipeline,
|
|
bson.M{"$sort": bson.D{
|
|
{Key: "rankScore", Value: -1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}},
|
|
bson.M{"$skip": skip},
|
|
bson.M{"$limit": limit},
|
|
bson.M{"$project": playletRankProjection()},
|
|
)
|
|
return pipeline
|
|
}
|
|
|
|
// playletTotalLikeRankBasePipeline 构建累计点赞榜基础过滤管道
|
|
func playletTotalLikeRankBasePipeline() []bson.M {
|
|
return []bson.M{
|
|
{"$match": bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}},
|
|
{"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.CountLike, 0}},
|
|
bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualCountLike, 0}},
|
|
}}}},
|
|
{"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
|
|
}
|
|
}
|
|
|
|
// playletPeriodLikeRankCountPipeline 构建周期新增点赞榜数量统计管道
|
|
func playletPeriodLikeRankCountPipeline(since time.Time) []bson.M {
|
|
return append(playletPeriodLikeRankBasePipeline(since), bson.M{"$count": "total"})
|
|
}
|
|
|
|
// playletPeriodLikeRankPagePipeline 构建周期新增点赞榜分页查询管道
|
|
func playletPeriodLikeRankPagePipeline(since time.Time, skip, limit int64) []bson.M {
|
|
pipeline := playletPeriodLikeRankBasePipeline(since)
|
|
pipeline = append(pipeline,
|
|
bson.M{"$sort": bson.D{
|
|
{Key: "rankScore", Value: -1},
|
|
{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}},
|
|
bson.M{"$skip": skip},
|
|
bson.M{"$limit": limit},
|
|
bson.M{"$project": playletRankProjection()},
|
|
)
|
|
return pipeline
|
|
}
|
|
|
|
// playletPeriodLikeRankBasePipeline 构建周期新增点赞榜基础过滤管道
|
|
func playletPeriodLikeRankBasePipeline(since time.Time) []bson.M {
|
|
return []bson.M{
|
|
{"$match": bson.M{playletlikemod.FN.PlayletLike.CreatedAt: bson.M{"$gte": since}}},
|
|
{"$group": bson.M{
|
|
"_id": "$" + playletlikemod.FN.PlayletLike.PlayletID,
|
|
"rankScore": bson.M{"$sum": 1},
|
|
}},
|
|
{"$lookup": bson.M{
|
|
"from": playletmod.TN.Playlet,
|
|
"localField": "_id",
|
|
"foreignField": playletmod.FN.Playlet.Id,
|
|
"as": "playlet",
|
|
}},
|
|
{"$unwind": "$playlet"},
|
|
{"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
|
|
{"$replaceRoot": bson.M{"newRoot": bson.M{"$mergeObjects": []interface{}{"$playlet", bson.M{"rankScore": "$rankScore"}}}}},
|
|
}
|
|
}
|
|
|
|
// playletRankProjection 限定排行榜返回字段,减少分页查询的数据传输体积
|
|
func playletRankProjection() bson.M {
|
|
return bson.M{
|
|
playletmod.FN.Playlet.Id: 1,
|
|
playletmod.FN.Playlet.Status: 1,
|
|
playletmod.FN.Playlet.IsRecommend: 1,
|
|
playletmod.FN.Playlet.OnlineAt: 1,
|
|
playletmod.FN.Playlet.MediaCreatedAt: 1,
|
|
playletmod.FN.Playlet.MediaUpdatedAt: 1,
|
|
playletmod.FN.Playlet.Title: 1,
|
|
playletmod.FN.Playlet.Slug: 1,
|
|
playletmod.FN.Playlet.Desc: 1,
|
|
playletmod.FN.Playlet.Anchor: 1,
|
|
playletmod.FN.Playlet.Cover: 1,
|
|
playletmod.FN.Playlet.CompressedCover: 1,
|
|
playletmod.FN.Playlet.Preview: 1,
|
|
playletmod.FN.Playlet.CategoryIds: 1,
|
|
playletmod.FN.Playlet.TagIds: 1,
|
|
playletmod.FN.Playlet.ThemeIds: 1,
|
|
playletmod.FN.Playlet.ContentType: 1,
|
|
playletmod.FN.Playlet.PayType: 1,
|
|
playletmod.FN.Playlet.SingleCoins: 1,
|
|
playletmod.FN.Playlet.TotalEpisode: 1,
|
|
playletmod.FN.Playlet.UpdateStatus: 1,
|
|
playletmod.FN.Playlet.CountBrowse: 1,
|
|
playletmod.FN.Playlet.VirtualCountBrowse: 1,
|
|
playletmod.FN.Playlet.HeatValue: 1,
|
|
playletmod.FN.Playlet.VirtualHeatValue: 1,
|
|
playletmod.FN.Playlet.CountCollect: 1,
|
|
playletmod.FN.Playlet.CountLike: 1,
|
|
playletmod.FN.Playlet.VirtualCountLike: 1,
|
|
playletmod.FN.Playlet.SeoInfo.Title: 1,
|
|
playletmod.FN.Playlet.SeoInfo.Description: 1,
|
|
playletmod.FN.Playlet.SeoInfo.Keyworld: 1,
|
|
playletmod.FN.Playlet.CreatedAt: 1,
|
|
playletmod.FN.Playlet.UpdatedAt: 1,
|
|
"rankScore": 1,
|
|
}
|
|
}
|
|
|
|
// playletRankAggItemsToPlayletList 将排行榜聚合数据转换为短剧列表,复用标签题材批量查询
|
|
func playletRankAggItemsToPlayletList(list []playletRankAggItem) playletmod.PlayletList {
|
|
result := make(playletmod.PlayletList, 0, len(list))
|
|
for i := range list {
|
|
result = append(result, &list[i].Playlet)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// playletRankToData 将排行榜聚合结果转换为榜单展示数据
|
|
func playletRankToData(item playletRankAggItem, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) proto.PlayletRankData {
|
|
return proto.PlayletRankData{
|
|
PlayletData: playletToAppData(&item.Playlet, imgCdn, tagNameMap, themeNameMap),
|
|
RankScore: item.RankScore,
|
|
}
|
|
}
|
|
|
|
// playletToAppData 将短剧模型转换为 app 短剧基础数据
|
|
func playletToAppData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) proto.PlayletData {
|
|
if item == nil {
|
|
return proto.PlayletData{}
|
|
}
|
|
return proto.PlayletData{
|
|
Id: item.Id,
|
|
Status: item.Status,
|
|
IsRecommend: item.IsRecommend,
|
|
IsNew: isNewPlaylet(item.OnlineAt),
|
|
IsFeatured: item.IsFeatured,
|
|
IsOriginal: isOriginalPlaylet(item),
|
|
ShowOriginal: item.ShowOriginal,
|
|
OnlineAt: item.OnlineAt,
|
|
MediaCreatedAt: item.MediaCreatedAt,
|
|
MediaUpdatedAt: item.MediaUpdatedAt,
|
|
Title: item.Title,
|
|
Slug: item.Slug,
|
|
Desc: item.Desc,
|
|
Anchor: item.Anchor,
|
|
Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
|
|
Preview: netutil.MergeURL(imgCdn, strings.TrimSpace(item.Preview)),
|
|
TagIds: item.TagIds,
|
|
TagNames: playletTagNamesFromMap(item.TagIds, tagNameMap),
|
|
TagSlugs: playletTagSlugsFromMap(item.TagIds, tagNameMap),
|
|
ThemeIds: item.ThemeIds,
|
|
ThemeNames: playletThemeNamesFromMap(item.ThemeIds, themeNameMap),
|
|
ContentType: item.ContentType,
|
|
PayType: item.PayType,
|
|
SingleCoins: item.SingleCoins,
|
|
TotalEpisode: item.TotalEpisode,
|
|
UpdateStatus: item.UpdateStatus,
|
|
CountBrowse: item.DisplayCountBrowse(),
|
|
HeatValue: item.DisplayHeatValue(),
|
|
CountCollect: item.CountCollect,
|
|
CountLike: item.CountLike,
|
|
VirtualCountLike: item.VirtualCountLike,
|
|
DisplayCountLike: item.CountLike + item.VirtualCountLike,
|
|
CreatedAt: item.CreatedAt,
|
|
UpdatedAt: item.UpdatedAt,
|
|
SeoInfo: normalizePlayletSeoValue(item.SeoInfo),
|
|
}
|
|
}
|
|
|
|
type playletHistoryRow struct {
|
|
ChapterID int64 `bson:"chapterId"`
|
|
Episode int64 `bson:"episode"`
|
|
UpdatedAt time.Time `bson:"updatedAt"`
|
|
Playlet playletmod.Playlet `bson:"playlet"`
|
|
}
|
|
|
|
// ReportPlayletHistory 校验上架短剧和剧集后写入当前用户观看历史
|
|
func ReportPlayletHistory(ctx context.Context, userID int64, req *proto.PlayletHistoryReportReq) (*proto.PlayletHistoryReportResp, *srvdef.SrvErr) {
|
|
if userID <= 0 || req == nil || req.PlayletID <= 0 || req.ChapterID <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
|
|
}
|
|
playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: req.PlayletID, playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: req.ChapterID,
|
|
playletchaptermod.FN.PlayletChapter.PlayletId: req.PlayletID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !playlet.IsOfficialEpisodeOpen(chapter.CurrentEpisode) {
|
|
return nil, srvdef.New(srvdef.ErrNoData, "本集暂未在官网开放", "")
|
|
}
|
|
if err := ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
|
|
return nil, err
|
|
}
|
|
if chapter.CurrentEpisode <= 0 || strings.TrimSpace(chapter.VideoUrl) == "" && strings.TrimSpace(chapter.WatermarkVideoUrl) == "" {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "剧集不可播放", "")
|
|
}
|
|
now := time.Now().UTC()
|
|
if err := models.TouchWatchHistory(ctx, models.WatchHistory{
|
|
UserID: userID, DataType: models.WatchHistoryTypePlaylet,
|
|
ContentID: req.PlayletID, ChapterID: req.ChapterID, Episode: chapter.CurrentEpisode,
|
|
CreatedAt: now, UpdatedAt: now,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.PlayletHistoryReportResp{WatchedAt: now}, nil
|
|
}
|
|
|
|
// QueryPlayletHistoryList 按最近观看时间倒序分页返回当前用户的上架短剧
|
|
func QueryPlayletHistoryList(ctx context.Context, userID int64, req *proto.PlayletHistoryListReq) (*proto.PlayletHistoryListResp, *srvdef.SrvErr) {
|
|
if userID <= 0 || req == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
|
|
}
|
|
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
|
|
base := []bson.M{
|
|
{"$match": bson.M{models.WatchHistoryUserID: userID, models.WatchHistoryDataType: models.WatchHistoryTypePlaylet, models.WatchHistoryUpdatedAt: bson.M{"$gte": time.Now().UTC().AddDate(0, 0, -models.WatchHistoryRetentionDays)}}},
|
|
{"$lookup": bson.M{"from": playletmod.TN.Playlet, "localField": models.WatchHistoryContentID, "foreignField": playletmod.FN.Playlet.Id, "as": "playlet"}},
|
|
{"$unwind": "$playlet"},
|
|
{"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
|
|
}
|
|
countRows := make([]struct {
|
|
Total int64 `bson:"total"`
|
|
}, 0)
|
|
if err := models.AggregateWatchHistory(ctx, &countRows, append(append([]bson.M(nil), base...), bson.M{"$count": "total"})); err != nil {
|
|
return nil, err
|
|
}
|
|
total := int64(0)
|
|
if len(countRows) > 0 {
|
|
total = countRows[0].Total
|
|
}
|
|
resp := &proto.PlayletHistoryListResp{BasePageResp: response.BasePageResp{
|
|
TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletHistoryItem, 0),
|
|
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
|
|
if total == 0 || pageNum > (total-1)/pageSize+1 {
|
|
return resp, nil
|
|
}
|
|
sortStage := bson.M{"$sort": bson.D{{Key: models.WatchHistoryUpdatedAt, Value: -1}, {Key: "_id", Value: -1}}}
|
|
pipeline := append([]bson.M{base[0], sortStage}, base[1:]...)
|
|
pipeline = append(pipeline, bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize})
|
|
rows := make([]playletHistoryRow, 0)
|
|
if err := models.AggregateWatchHistory(ctx, &rows, pipeline); err != nil {
|
|
return nil, err
|
|
}
|
|
list := make(playletmod.PlayletList, 0, len(rows))
|
|
for i := range rows {
|
|
list = append(list, &rows[i].Playlet)
|
|
}
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cdn := configser.GetRequestConfig(ctx).ImgCdn
|
|
items := make([]*proto.PlayletHistoryItem, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, &proto.PlayletHistoryItem{
|
|
PlayletData: playletToAppData(&row.Playlet, cdn, tagNames, themeNames),
|
|
ChapterID: row.ChapterID, Episode: row.Episode, WatchedAt: row.UpdatedAt,
|
|
})
|
|
}
|
|
resp.List = items
|
|
return resp, nil
|
|
}
|
|
|
|
// ClearPlayletHistory 清空当前用户的短剧观看历史
|
|
func ClearPlayletHistory(ctx context.Context, userID int64) (*proto.PlayletHistoryClearResp, *srvdef.SrvErr) {
|
|
if userID <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "用户无效", "")
|
|
}
|
|
count, err := models.DeleteWatchHistory(ctx, userID, models.WatchHistoryTypePlaylet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &proto.PlayletHistoryClearResp{DeletedCount: count}, nil
|
|
}
|
|
|
|
// buildPlayletTagThemeNameMaps 批量查询当前页短剧标签和题材名称,避免列表循环内多次访问数据库
|
|
func buildPlayletTagThemeNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string, *srvdef.SrvErr) {
|
|
tagIds := uniquePlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
|
|
return item.TagIds
|
|
})
|
|
themeIds := uniquePlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
|
|
return item.ThemeIds
|
|
})
|
|
tagNameMap, err := buildPlayletTagNameMap(ctx, tagIds)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
themeNameMap, err := buildPlayletThemeNameMap(ctx, themeIds)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return tagNameMap, themeNameMap, nil
|
|
}
|
|
|
|
// uniquePlayletRelationIds 按短剧关联字段收集去重后的ID
|
|
func uniquePlayletRelationIds(list playletmod.PlayletList, values func(item *playletmod.Playlet) []int64) []int64 {
|
|
ids := make([]int64, 0)
|
|
seen := make(map[int64]struct{})
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
for _, id := range values(item) {
|
|
if id <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// buildPlayletTagNameMap 批量查询当前页短剧标签名称
|
|
func buildPlayletTagNameMap(ctx context.Context, tagIds []int64) (contenttagmod.ContentTagMap, *srvdef.SrvErr) {
|
|
result := make(contenttagmod.ContentTagMap, len(tagIds))
|
|
if len(tagIds) == 0 {
|
|
return result, nil
|
|
}
|
|
return contenttagmod.GetMapTags(ctx, tagIds)
|
|
}
|
|
|
|
// buildPlayletThemeNameMap 批量查询当前页短剧题材名称
|
|
func buildPlayletThemeNameMap(ctx context.Context, themeIds []int64) (map[int64]string, *srvdef.SrvErr) {
|
|
result := make(map[int64]string, len(themeIds))
|
|
if len(themeIds) == 0 {
|
|
return result, nil
|
|
}
|
|
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
|
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
|
|
}, options.Find().SetProjection(bson.M{
|
|
playletthememod.FN.PlayletTheme.Id: 1,
|
|
playletthememod.FN.PlayletTheme.Name: 1,
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, theme := range themes {
|
|
if theme != nil {
|
|
result[theme.Id] = theme.Name
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// playletTagNamesFromMap 按短剧标签ID原始顺序组装标签名称
|
|
func playletTagNamesFromMap(tagIds []int64, tagNameMap contenttagmod.ContentTagMap) []string {
|
|
result := make([]string, 0, len(tagIds))
|
|
for _, tagId := range tagIds {
|
|
if tag := tagNameMap[tagId]; tag != nil && tag.Name != "" {
|
|
result = append(result, tag.Name)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// playletTagSlugsFromMap 按标签名称的顺序组装slug
|
|
func playletTagSlugsFromMap(tagIds []int64, tagNameMap contenttagmod.ContentTagMap) []string {
|
|
result := make([]string, 0, len(tagIds))
|
|
for _, tagId := range tagIds {
|
|
if tag := tagNameMap[tagId]; tag != nil && tag.Name != "" {
|
|
result = append(result, tag.Slug)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// playletThemeNamesFromMap 按短剧题材ID原始顺序组装题材名称
|
|
func playletThemeNamesFromMap(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
|
|
}
|
|
|
|
// isNewPlaylet 判断短剧是否为本周内上架
|
|
func isNewPlaylet(onlineAt time.Time) bool {
|
|
if onlineAt.IsZero() {
|
|
return false
|
|
}
|
|
return onlineAt.After(utils.BeginOfWeekByTime(utils.Now()))
|
|
}
|
|
|
|
// normalizePlayletSeoValue 规范化 SEO 字段,避免 keywords 为 null
|
|
func normalizePlayletSeoValue(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
|
|
}
|
|
|
|
// playletRequestPath 从上下文中提取当前请求路径,避免API层重复组装短剧响应字段
|
|
func playletRequestPath(ctx context.Context) string {
|
|
if ginCtx, ok := ctx.(*gin.Context); ok && ginCtx.Request != nil && ginCtx.Request.URL != nil {
|
|
return ginCtx.Request.URL.Path
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// emptyPlayletRecommendListPage 构造 app 推荐短剧空分页响应
|
|
func emptyPlayletRecommendListPage(pageNum, pageSize int64) *proto.PlayletRecommendListResp {
|
|
return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: make([]*proto.PlayletRecommendListItem, 0),
|
|
}}
|
|
}
|
|
|
|
const (
|
|
playletHomeBannerLimit int64 = 5
|
|
playletHomeModuleTopicLimit int64 = 4
|
|
playletHomeTopicLimit int64 = 3
|
|
playletHomeTopicItems int64 = 3
|
|
)
|
|
|
|
// buildHomePlayletResponse 查询并组装短剧首页数据
|
|
func buildHomePlayletResponse(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
|
|
cfg := configser.GetRequestConfig(ctx)
|
|
// 广告和 SEO 使用当前请求的站点配置
|
|
seo := pubdef.SeoValue{Title: cfg.WebTitle, Keyworld: cfg.WebKeyworld, Description: cfg.WebDescription}
|
|
resp := &proto.HomePlayletResponse{
|
|
HomeBannerData: make([]*proto.HomeBannerData, 0),
|
|
Modules: make([]*proto.HomeModuleData, 0),
|
|
Announcements: make([]*proto.AnnouncementItem, 0),
|
|
Advitise: configser.GetAdvitise(),
|
|
SeoInfo: &seo,
|
|
}
|
|
// Banner 只取已上架且开启首页展示的短剧,优先展示置顶权重高的内容
|
|
banners, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
playletmod.FN.Playlet.ShowOnBanner: true,
|
|
}, options.Find().SetSort(bson.D{
|
|
{Key: playletmod.FN.Playlet.TopRank, Value: -1},
|
|
{Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
|
|
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
|
}).SetLimit(playletHomeBannerLimit))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, banners)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range banners {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
resp.HomeBannerData = append(resp.HomeBannerData, &proto.HomeBannerData{
|
|
PlayletData: playletToAppData(item, cfg.ImgCdn, tagNames, themeNames),
|
|
ShowOnBanner: item.ShowOnBanner,
|
|
Banner: netutil.MergeURL(cfg.ImgCdn, item.Banner),
|
|
})
|
|
}
|
|
// 首页模块仅使用已启用的配置,排序值越大越靠前
|
|
configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx,
|
|
bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true},
|
|
options.Find().SetSort(bson.D{
|
|
{Key: playlethomeconfigmod.FN.PlayletHomeConfig.Rank, Value: -1},
|
|
{Key: playlethomeconfigmod.FN.PlayletHomeConfig.Id, Value: -1},
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range configs {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
module, err := homeModule(ctx, item, cfg.ImgCdn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Modules = append(resp.Modules, module)
|
|
}
|
|
// 公告只返回启用项,先按排序值再按修改时间倒序排列
|
|
announcements, err := announcementmod.FindManyAnnouncement(ctx,
|
|
bson.M{announcementmod.FN.Announcement.Status: true},
|
|
options.Find().SetSort(bson.D{
|
|
{Key: announcementmod.FN.Announcement.Sort, Value: -1},
|
|
{Key: announcementmod.FN.Announcement.UpdatedAt, Value: -1},
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range announcements {
|
|
if item != nil {
|
|
resp.Announcements = append(resp.Announcements, &proto.AnnouncementItem{Id: item.Id, Title: item.Title, Content: item.Content})
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// homeModule 按配置的数据类型和模块类型组装首页模块
|
|
func homeModule(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) (*proto.HomeModuleData, *srvdef.SrvErr) {
|
|
// 未设置条数时使用默认值,并限制最大查询量
|
|
limit := config.Limit
|
|
if limit <= 0 {
|
|
limit = playlethomeconfigmod.DefaultLimit
|
|
}
|
|
if limit > playlethomeconfigmod.MaxLimit {
|
|
limit = playlethomeconfigmod.MaxLimit
|
|
}
|
|
module := &proto.HomeModuleData{
|
|
Id: config.Id,
|
|
Slug: config.Slug,
|
|
Title: config.Title,
|
|
Desc: config.Desc,
|
|
Label: config.Label,
|
|
Style: config.Style,
|
|
CardBackgroundImage: netutil.MergeURL(imgCdn, config.CardBackgroundImage),
|
|
ModuleType: config.ModuleType,
|
|
DataType: config.DataType,
|
|
RecommendType: config.RecommendType,
|
|
CategoryId: config.CategoryId,
|
|
Rank: config.Rank,
|
|
List: make([]*proto.PlayletData, 0),
|
|
TopicList: make([]*proto.HomeTopicData, 0),
|
|
}
|
|
// 专题模块按数据类型分别使用视频分类或短剧题材
|
|
if config.DataType == playlethomeconfigmod.DataTypeVideo {
|
|
if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
|
|
topics, err := homeVideoTopics(ctx, config, imgCdn)
|
|
module.TopicList = topics
|
|
return module, err
|
|
}
|
|
list, err := homeVideos(ctx, config, imgCdn, limit, 0, 0)
|
|
module.List = list
|
|
return module, err
|
|
}
|
|
if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
|
|
topics, err := homeTopics(ctx, config, imgCdn)
|
|
module.TopicList = topics
|
|
return module, err
|
|
}
|
|
list, err := homePlaylets(ctx, config, 0, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
module.List, err = homePlayletData(ctx, list, imgCdn)
|
|
return module, err
|
|
}
|
|
|
|
// homePlaylets 查询已上架短剧,按需叠加分类和专题筛选
|
|
func homePlaylets(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, themeId, limit int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
|
filter, ok := homePlayletFilter(config, themeId)
|
|
if !ok {
|
|
return make(playletmod.PlayletList, 0), nil
|
|
}
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
return playletmod.FindManyPlayletByCategoryRank(ctx, filter, config.CategoryId, 0, limit, playletmod.CategoryRankSort(), nil)
|
|
}
|
|
return findPlayletRecommendPage(ctx, filter, homePlayletSort(config.RecommendType), 0, limit)
|
|
}
|
|
|
|
// homePlayletSort 将推荐类型映射到短剧排序字段,ID 用于稳定同分排序
|
|
func homePlayletSort(recommendType int64) bson.D {
|
|
var primary string
|
|
switch recommendType {
|
|
case playlethomeconfigmod.RecommendTypeUpdated:
|
|
primary = playletmod.FN.Playlet.UpdatedAt
|
|
case playlethomeconfigmod.RecommendTypeLike:
|
|
primary = playletmod.FN.Playlet.CountLike
|
|
case playlethomeconfigmod.RecommendTypeHeat:
|
|
primary = playletmod.FN.Playlet.HeatValue
|
|
case playlethomeconfigmod.RecommendTypeCategory:
|
|
primary = playletmod.FN.Playlet.RecommendRank
|
|
default:
|
|
primary = playletmod.FN.Playlet.CountBrowse
|
|
}
|
|
return bson.D{{Key: primary, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
}
|
|
|
|
// homePlayletData 补齐标签和专题名称并转换为 App 短剧结构
|
|
func homePlayletData(ctx context.Context, list playletmod.PlayletList, imgCdn string) ([]*proto.PlayletData, *srvdef.SrvErr) {
|
|
result := make([]*proto.PlayletData, 0, len(list))
|
|
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
data := playletToAppData(item, imgCdn, tagNames, themeNames)
|
|
result = append(result, &data)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// homeVideos 查询审核通过的视频,推荐类型决定排序或分类筛选
|
|
func homeVideos(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string, limit, skip, categoryId int64) ([]*proto.HomeVideoData, *srvdef.SrvErr) {
|
|
result := make([]*proto.HomeVideoData, 0)
|
|
filter, ok := homeVideoFilter(config, categoryId)
|
|
if !ok {
|
|
return result, nil
|
|
}
|
|
sortKey := videoinformationmod.FN.VideoInformation.Real.WatchTimes
|
|
switch config.RecommendType {
|
|
case playlethomeconfigmod.RecommendTypeUpdated:
|
|
sortKey = videoinformationmod.FN.VideoInformation.UpdatedAt
|
|
case playlethomeconfigmod.RecommendTypeLike:
|
|
sortKey = videoinformationmod.FN.VideoInformation.Real.Likes
|
|
case playlethomeconfigmod.RecommendTypeHeat:
|
|
sortKey = videoinformationmod.FN.VideoInformation.HeatValue
|
|
case playlethomeconfigmod.RecommendTypeCategory:
|
|
sortKey = videoinformationmod.FN.VideoInformation.Sort
|
|
}
|
|
sort := bson.D{{Key: sortKey, Value: -1}}
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeHeat {
|
|
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1})
|
|
}
|
|
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1})
|
|
projection := bson.D{
|
|
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Desc, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
|
|
{Key: videoinformationmod.FN.VideoInformation.Fake.Likes, Value: 1},
|
|
}
|
|
var list videoinformationmod.VideoInformationList
|
|
var err *srvdef.SrvErr
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
selectedCategoryId := config.CategoryId
|
|
if categoryId > 0 {
|
|
selectedCategoryId = categoryId
|
|
}
|
|
project := bson.M{}
|
|
for _, field := range projection {
|
|
project[field.Key] = field.Value
|
|
}
|
|
list, err = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, selectedCategoryId, skip, limit, project)
|
|
} else {
|
|
list, err = videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().SetProjection(projection).SetSort(sort).SetSkip(skip).SetLimit(limit))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 展示数值为真实统计与后台设置的虚拟统计之和
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
result = append(result, &proto.HomeVideoData{
|
|
Id: item.Id, Title: item.Title, Slug: item.Slug,
|
|
Cover: netutil.MergeURL(imgCdn, item.CoverImg), Desc: item.Desc,
|
|
Tags: item.Tags, WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
|
|
LikeNum: item.Real.Likes + item.Fake.Likes,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// homeVideoTopics 从配置的视频分类中按视频数量选取前四个有内容的专题
|
|
func homeVideoTopics(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) ([]*proto.HomeTopicData, *srvdef.SrvErr) {
|
|
topics, _, err := homeTopicPage(ctx, config, imgCdn, 0, playletHomeModuleTopicLimit, 0, true, true)
|
|
return topics, err
|
|
}
|
|
|
|
// homeTopics 从配置的题材中按上架短剧数量选取前四个专题,每个专题最多取三部短剧
|
|
func homeTopics(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) ([]*proto.HomeTopicData, *srvdef.SrvErr) {
|
|
topics, _, err := homeTopicPage(ctx, config, imgCdn, 0, playletHomeModuleTopicLimit, 0, true, true)
|
|
return topics, err
|
|
}
|
|
|
|
const (
|
|
homeConfigPageSize int64 = 20
|
|
homeTopicPageSize int64 = 12
|
|
homePageMaxSize int64 = 100
|
|
homePlayletCacheTTL = 5 * time.Minute
|
|
)
|
|
|
|
// HomeConfigList 按首页配置分页查询短剧、视频或专题
|
|
func HomeConfigList(ctx context.Context, req *proto.PlayletHomeConfigListReq) (*proto.PlayletHomeConfigListResp, *srvdef.SrvErr) {
|
|
if req == nil || (req.Id <= 0 && strings.TrimSpace(req.Slug) == "") {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "首页配置ID或slug不能为空", "")
|
|
}
|
|
filter := bson.M{}
|
|
if slug := strings.TrimSpace(req.Slug); slug != "" {
|
|
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
|
|
} else {
|
|
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = req.Id
|
|
}
|
|
config, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pageNum, pageSize := normalizeHomePage(req.PageNum, req.PageSize, homeConfigPageSize)
|
|
resp := &proto.PlayletHomeConfigListResp{
|
|
Config: &proto.HomeModuleData{
|
|
Id: config.Id, Slug: config.Slug, Title: config.Title, Desc: config.Desc, Label: config.Label,
|
|
Style: config.Style, CardBackgroundImage: netutil.MergeURL(configser.GetRequestConfig(ctx).ImgCdn, config.CardBackgroundImage),
|
|
ModuleType: config.ModuleType, DataType: config.DataType, RecommendType: config.RecommendType,
|
|
CategoryId: config.CategoryId, Rank: config.Rank,
|
|
List: make([]*proto.PlayletData, 0), TopicList: make([]*proto.HomeTopicData, 0),
|
|
},
|
|
PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
|
|
TopicList: make([]*proto.HomeTopicData, 0), Advitise: configser.GetAdvitise(),
|
|
}
|
|
if !config.Enabled {
|
|
return resp, nil
|
|
}
|
|
skip := (pageNum - 1) * pageSize
|
|
imgCdn := configser.GetRequestConfig(ctx).ImgCdn
|
|
if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
|
|
resp.TopicList, resp.TotalCount, err = homeTopicPage(ctx, config, imgCdn, skip, pageSize, 0, true, true)
|
|
return resp, err
|
|
}
|
|
if config.DataType == playlethomeconfigmod.DataTypeVideo {
|
|
filter, ok := homeVideoFilter(config, 0)
|
|
if !ok {
|
|
resp.List = make([]*proto.HomeVideoData, 0)
|
|
return resp, nil
|
|
}
|
|
resp.TotalCount, err = videoinformationmod.CountVideoInformations(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.List, err = homeVideos(ctx, config, imgCdn, pageSize, skip, 0)
|
|
return resp, err
|
|
}
|
|
filter, ok := homePlayletFilter(config, 0)
|
|
if !ok {
|
|
return resp, nil
|
|
}
|
|
resp.TotalCount, err = playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var list playletmod.PlayletList
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, config.CategoryId, skip, pageSize, playletmod.CategoryRankSort(), nil)
|
|
} else {
|
|
list, err = findPlayletRecommendPage(ctx, filter, homePlayletSort(config.RecommendType), skip, pageSize)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.List, err = homePlayletData(ctx, list, imgCdn)
|
|
return resp, err
|
|
}
|
|
|
|
// TopicDetail 根据数据类型查询短剧题材或视频分类专题详情
|
|
func TopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq) (*proto.PlayletTopicDetailResp, *srvdef.SrvErr) {
|
|
if req == nil || (req.Id <= 0 && strings.TrimSpace(req.TopicSlug) == "") {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "专题ID或slug不能为空", "")
|
|
}
|
|
dataType := req.DataType
|
|
if dataType == 0 {
|
|
dataType = playlethomeconfigmod.DataTypeAll
|
|
}
|
|
if dataType != playlethomeconfigmod.DataTypeAll && dataType != playlethomeconfigmod.DataTypeVideo {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "数据类型无效", "")
|
|
}
|
|
pageNum, pageSize := normalizeHomePage(req.PageNum, req.PageSize, homeTopicPageSize)
|
|
imgCdn := configser.GetRequestConfig(ctx).ImgCdn
|
|
resp := &proto.PlayletTopicDetailResp{
|
|
DataType: dataType, PageNum: int(pageNum), PageSize: int(pageSize),
|
|
Covers: make([]string, 0), List: make([]*proto.PlayletData, 0),
|
|
MoreTopics: make([]*proto.HomeTopicData, 0), Advitise: configser.GetAdvitise(),
|
|
}
|
|
skip := (pageNum - 1) * pageSize
|
|
var err *srvdef.SrvErr
|
|
if dataType == playlethomeconfigmod.DataTypeVideo {
|
|
err = fillVideoTopicDetail(ctx, req, resp, imgCdn, skip, pageSize)
|
|
} else {
|
|
err = fillPlayletTopicDetail(ctx, req, resp, imgCdn, skip, pageSize)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := &playlethomeconfigmod.PlayletHomeConfig{DataType: dataType, RecommendType: playlethomeconfigmod.RecommendTypeHeat}
|
|
resp.MoreTopics, _, err = homeTopicPage(ctx, config, imgCdn, 0, playletHomeTopicLimit, resp.Id, true, false)
|
|
return resp, err
|
|
}
|
|
|
|
func fillPlayletTopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq, resp *proto.PlayletTopicDetailResp, imgCdn string, skip, pageSize int64) *srvdef.SrvErr {
|
|
filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
|
|
if slug := strings.TrimSpace(req.TopicSlug); slug != "" {
|
|
filter[playletthememod.FN.PlayletTheme.Slug] = slug
|
|
} else {
|
|
filter[playletthememod.FN.PlayletTheme.Id] = req.Id
|
|
}
|
|
theme, err := playletthememod.FindOnePlayletTheme(ctx, filter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Id, resp.Name, resp.Slug, resp.Rank = theme.Id, theme.Name, theme.Slug, theme.Rank
|
|
resp.Desc, resp.Icon = theme.SeoInfo.Description, netutil.MergeURL(imgCdn, theme.Icon)
|
|
seo := theme.SeoInfo
|
|
resp.SeoInfo = &seo
|
|
playletFilter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp, playletmod.FN.Playlet.ThemeIds: theme.Id}
|
|
resp.TotalCount, err = playletmod.CountPlaylet(ctx, playletFilter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.PlayletCount = resp.TotalCount
|
|
sortOrder := homeTopicDetailPlayletSort(req.SortType)
|
|
covers, err := findPlayletRecommendPage(ctx, playletFilter, homeTopicDetailPlayletSort(1), 0, playletHomeTopicItems)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range covers {
|
|
if item != nil && item.Cover != "" {
|
|
resp.Covers = append(resp.Covers, netutil.MergeURL(imgCdn, item.Cover))
|
|
}
|
|
}
|
|
list, err := findPlayletRecommendPage(ctx, playletFilter, sortOrder, skip, pageSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.List, err = homePlayletData(ctx, list, imgCdn)
|
|
return err
|
|
}
|
|
|
|
func fillVideoTopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq, resp *proto.PlayletTopicDetailResp, imgCdn string, skip, pageSize int64) *srvdef.SrvErr {
|
|
filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
|
|
if slug := strings.TrimSpace(req.TopicSlug); slug != "" {
|
|
filter[videocategorymod.FN.VideoCategory.Slug] = slug
|
|
} else {
|
|
filter[videocategorymod.FN.VideoCategory.Id] = req.Id
|
|
}
|
|
category, err := videocategorymod.FindOneVideoCategory(ctx, filter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Id, resp.Name, resp.Slug, resp.Rank = category.Id, category.Name, category.Slug, category.Rank
|
|
resp.Desc = category.Desc
|
|
seo := category.SeoInfo
|
|
resp.SeoInfo = &seo
|
|
videoFilter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
videoinformationmod.FN.VideoInformation.CategoryId: category.Id}
|
|
resp.TotalCount, err = videoinformationmod.CountVideoInformations(ctx, videoFilter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.VideoCount = resp.TotalCount
|
|
config := &playlethomeconfigmod.PlayletHomeConfig{RecommendType: homeTopicDetailVideoRecommendType(req.SortType)}
|
|
covers, err := homeVideos(ctx, &playlethomeconfigmod.PlayletHomeConfig{RecommendType: playlethomeconfigmod.RecommendTypeHeat}, imgCdn, playletHomeTopicItems, 0, category.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range covers {
|
|
if item != nil && item.Cover != "" {
|
|
resp.Covers = append(resp.Covers, item.Cover)
|
|
}
|
|
}
|
|
resp.List, err = homeVideos(ctx, config, imgCdn, pageSize, skip, category.Id)
|
|
return err
|
|
}
|
|
|
|
func homeTopicDetailPlayletSort(sortType int64) bson.D {
|
|
switch sortType {
|
|
case 2:
|
|
return bson.D{{Key: playletmod.FN.Playlet.OnlineAt, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
case 3:
|
|
return bson.D{{Key: playletmod.FN.Playlet.HeatValue, Value: -1}, {Key: playletmod.FN.Playlet.CountBrowse, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
default:
|
|
return bson.D{{Key: playletmod.FN.Playlet.RecommendRank, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
|
}
|
|
}
|
|
|
|
func homeTopicDetailVideoRecommendType(sortType int64) int64 {
|
|
switch sortType {
|
|
case 2:
|
|
return playlethomeconfigmod.RecommendTypeUpdated
|
|
case 3:
|
|
return playlethomeconfigmod.RecommendTypeBrowse
|
|
default:
|
|
return playlethomeconfigmod.RecommendTypeCategory
|
|
}
|
|
}
|
|
|
|
func normalizeHomePage(pageNum, pageSize, defaultSize int64) (int64, int64) {
|
|
if pageNum <= 0 {
|
|
pageNum = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = defaultSize
|
|
}
|
|
if pageSize > homePageMaxSize {
|
|
pageSize = homePageMaxSize
|
|
}
|
|
return pageNum, pageSize
|
|
}
|
|
|
|
func homePlayletFilter(config *playlethomeconfigmod.PlayletHomeConfig, themeId int64) (bson.M, bool) {
|
|
filter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
if config.CategoryId <= 0 {
|
|
return nil, false
|
|
}
|
|
filter[playletmod.FN.Playlet.CategoryIds] = config.CategoryId
|
|
}
|
|
if themeId > 0 {
|
|
filter[playletmod.FN.Playlet.ThemeIds] = themeId
|
|
}
|
|
return filter, true
|
|
}
|
|
|
|
func homeVideoFilter(config *playlethomeconfigmod.PlayletHomeConfig, categoryId int64) (bson.M, bool) {
|
|
filter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}
|
|
if categoryId > 0 {
|
|
filter[videoinformationmod.FN.VideoInformation.CategoryId] = categoryId
|
|
} else if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
if config.CategoryId <= 0 {
|
|
return nil, false
|
|
}
|
|
filter[videoinformationmod.FN.VideoInformation.CategoryId] = config.CategoryId
|
|
}
|
|
return filter, true
|
|
}
|
|
|
|
// homeTopicPage 按专题排序分页,可分别限定有上架内容和首页配置关联的专题
|
|
func homeTopicPage(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string, skip, limit, excludeId int64, onlyWithContent, onlyConfigured bool) ([]*proto.HomeTopicData, int64, *srvdef.SrvErr) {
|
|
type topicStat struct {
|
|
id, rank, count int64
|
|
name, slug, icon string
|
|
seo pubdef.SeoValue
|
|
}
|
|
stats := make([]topicStat, 0)
|
|
selectedTopics := make(map[int64]struct{}, len(config.TopicIds))
|
|
if onlyConfigured {
|
|
for _, id := range config.TopicIds {
|
|
selectedTopics[id] = struct{}{}
|
|
}
|
|
if len(selectedTopics) == 0 {
|
|
return make([]*proto.HomeTopicData, 0), 0, nil
|
|
}
|
|
}
|
|
if config.DataType == playlethomeconfigmod.DataTypeVideo {
|
|
filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
|
|
if onlyConfigured {
|
|
filter[videocategorymod.FN.VideoCategory.Id] = bson.M{"$in": config.TopicIds}
|
|
}
|
|
if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
|
|
if config.CategoryId <= 0 {
|
|
return make([]*proto.HomeTopicData, 0), 0, nil
|
|
}
|
|
filter[videocategorymod.FN.VideoCategory.Id] = config.CategoryId
|
|
}
|
|
categories, err := videocategorymod.FindManyVideoCategorys(ctx, filter)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for _, category := range categories {
|
|
if category == nil || category.Id == excludeId {
|
|
continue
|
|
}
|
|
if onlyConfigured {
|
|
if _, ok := selectedTopics[category.Id]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
videoinformationmod.FN.VideoInformation.CategoryId: category.Id,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if onlyWithContent && count == 0 {
|
|
continue
|
|
}
|
|
stats = append(stats, topicStat{id: category.Id, rank: category.Rank, count: count,
|
|
name: category.Name, slug: category.Slug, seo: category.SeoInfo})
|
|
}
|
|
} else {
|
|
filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
|
|
if onlyConfigured {
|
|
filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$in": config.TopicIds}
|
|
}
|
|
themes, err := playletthememod.FindManyPlayletTheme(ctx, filter)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for _, theme := range themes {
|
|
if theme == nil || theme.Id == excludeId {
|
|
continue
|
|
}
|
|
if onlyConfigured {
|
|
if _, ok := selectedTopics[theme.Id]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
filter, ok := homePlayletFilter(config, theme.Id)
|
|
if !ok {
|
|
continue
|
|
}
|
|
count, err := playletmod.CountPlaylet(ctx, filter)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if onlyWithContent && count == 0 {
|
|
continue
|
|
}
|
|
stats = append(stats, topicStat{id: theme.Id, rank: theme.Rank, count: count,
|
|
name: theme.Name, slug: theme.Slug, icon: theme.Icon, seo: theme.SeoInfo})
|
|
}
|
|
}
|
|
sort.Slice(stats, func(i, j int) bool {
|
|
if stats[i].count != stats[j].count {
|
|
return stats[i].count > stats[j].count
|
|
}
|
|
if stats[i].rank != stats[j].rank {
|
|
return stats[i].rank > stats[j].rank
|
|
}
|
|
return stats[i].id > stats[j].id
|
|
})
|
|
total := int64(len(stats))
|
|
result := make([]*proto.HomeTopicData, 0)
|
|
if skip >= total || limit <= 0 {
|
|
return result, total, nil
|
|
}
|
|
end := skip + limit
|
|
if end > total {
|
|
end = total
|
|
}
|
|
if onlyWithContent {
|
|
end = total // 被排除的内容可能让专题变空,继续查后续专题补足展示数量
|
|
}
|
|
for _, stat := range stats[int(skip):int(end)] {
|
|
if onlyWithContent && int64(len(result)) >= limit {
|
|
break
|
|
}
|
|
item := &proto.HomeTopicData{Id: stat.id, Name: stat.name, Slug: stat.slug,
|
|
Icon: netutil.MergeURL(imgCdn, stat.icon), Rank: stat.rank, SeoInfo: stat.seo}
|
|
if config.DataType == playlethomeconfigmod.DataTypeVideo {
|
|
item.VideoCount = stat.count
|
|
list, err := homeVideos(ctx, config, imgCdn, playletHomeTopicItems, 0, stat.id)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
item.List = list
|
|
if onlyWithContent && len(list) == 0 {
|
|
continue
|
|
}
|
|
} else {
|
|
item.PlayletCount = stat.count
|
|
list, err := homePlaylets(ctx, config, stat.id, playletHomeTopicItems)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if onlyWithContent && len(list) == 0 {
|
|
continue
|
|
}
|
|
item.List, err = homePlayletData(ctx, list, imgCdn)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
// Home 优先返回 Redis 缓存,未命中或缓存不可用时查询数据库
|
|
func Home(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
|
|
key := homePlayletCacheKey(configser.GetRequestConfig(ctx).ImgCdn)
|
|
if resp := getHomePlayletCache(key); resp != nil {
|
|
return resp, nil
|
|
}
|
|
resp, err := buildHomePlayletResponse(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
setHomePlayletCache(key, resp)
|
|
return resp, nil
|
|
}
|
|
|
|
// homePlayletCacheKey 包含缓存版本和图片域名,后台变更或域名变更后使用新键
|
|
func homePlayletCacheKey(imgCdn string) string {
|
|
if redis.Client() == nil {
|
|
return ""
|
|
}
|
|
version := "0"
|
|
value, err := redis.Get(redisconst.AppPlayletHomeCacheVersionKey())
|
|
if err != nil {
|
|
log.Warn("读取短剧首页缓存版本失败", log.E(err))
|
|
return ""
|
|
}
|
|
if value != nil && strings.TrimSpace(*value) != "" {
|
|
version = strings.TrimSpace(*value)
|
|
}
|
|
cdnHash := sha256.Sum256([]byte(imgCdn))
|
|
return fmt.Sprintf("app:playlet:home:data:%d:v7:%s:%x", pubdef.AppId, version, cdnHash[:8])
|
|
}
|
|
|
|
// getHomePlayletCache 读取并反序列化完整首页响应
|
|
func getHomePlayletCache(key string) *proto.HomePlayletResponse {
|
|
if key == "" {
|
|
return nil
|
|
}
|
|
value, err := redis.Get(key)
|
|
if err != nil {
|
|
log.Warn("读取短剧首页缓存失败", log.E(err))
|
|
return nil
|
|
}
|
|
if value == nil || *value == "" {
|
|
return nil
|
|
}
|
|
var resp proto.HomePlayletResponse
|
|
if err := json.Unmarshal([]byte(*value), &resp); err != nil {
|
|
log.Warn("解析短剧首页缓存失败", log.E(err))
|
|
return nil
|
|
}
|
|
return &resp
|
|
}
|
|
|
|
// setHomePlayletCache 缓存完整首页响应五分钟
|
|
func setHomePlayletCache(key string, resp *proto.HomePlayletResponse) {
|
|
if key == "" || resp == nil {
|
|
return
|
|
}
|
|
data, err := json.Marshal(resp)
|
|
if err != nil {
|
|
log.Warn("序列化短剧首页缓存失败", log.E(err))
|
|
return
|
|
}
|
|
if err := redis.Set(key, string(data), homePlayletCacheTTL); err != nil {
|
|
log.Warn("写入短剧首页缓存失败", log.E(err))
|
|
}
|
|
}
|