@@ -0,0 +1,49 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"videoweb/pub/models/homeconfigmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func TestHomeConfigDisplayLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *homeconfigmod.HomeConfig
|
||||
want int64
|
||||
}{
|
||||
{name: "旧题材配置", config: &homeconfigmod.HomeConfig{Type: homeconfigmod.TypeTheme}, want: 8},
|
||||
{name: "自定义题材数量", config: &homeconfigmod.HomeConfig{Type: homeconfigmod.TypeTheme, Limit: 5}, want: 5},
|
||||
{name: "旧短剧配置", config: &homeconfigmod.HomeConfig{Type: homeconfigmod.TypePlaylet}, want: 12},
|
||||
{name: "自定义短剧数量", config: &homeconfigmod.HomeConfig{Type: homeconfigmod.TypePlaylet, Limit: 20}, want: 20},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := homeConfigDisplayLimit(tt.config); got != tt.want {
|
||||
t.Fatalf("homeConfigDisplayLimit() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeConfigSortTypeFourUsesOnlineAt(t *testing.T) {
|
||||
filter, sort := homeConfigPlayletQuery(homeconfigmod.SortTypeOnlineAt, 0)
|
||||
if _, ok := filter[playletmod.FN.Playlet.Preview]; ok {
|
||||
t.Fatal("上架时间排序不应过滤无预告片的短剧")
|
||||
}
|
||||
want := bson.D{
|
||||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||||
}
|
||||
if !reflect.DeepEqual(sort, want) {
|
||||
t.Fatalf("上架时间排序 = %v, want %v", sort, want)
|
||||
}
|
||||
|
||||
_, latestSort := homeConfigPlayletQuery(homeconfigmod.SortTypeLatest, 0)
|
||||
if len(latestSort) == 0 || latestSort[0].Key != playletmod.FN.Playlet.UpdatedAt {
|
||||
t.Fatalf("最近更新应按更新时间排序,得到 %v", latestSort)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/playletcache"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/homeconfigmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func homeConfigToModule(item *proto.HomeConfigData) *proto.HomeModuleData {
|
||||
moduleType := int32(1)
|
||||
if item.Type == homeconfigmod.TypeTheme {
|
||||
moduleType = 2
|
||||
}
|
||||
return &proto.HomeModuleData{Id: item.Id, Slug: item.Slug, Title: item.Name, Desc: item.Desc, Keyword: item.Keyword, Label: item.Label,
|
||||
Style: item.Style, CardBackgroundImage: item.CardBackgroundImage, ModuleType: moduleType, DataType: 1,
|
||||
RecommendType: item.RecommendType, CategoryId: item.CategoryId, Rank: item.Rank, Limit: item.Limit, SeoInfo: item.SeoInfo,
|
||||
List: item.List, TopicList: item.ThemeList}
|
||||
}
|
||||
|
||||
// recommendType=0 保留历史 sortType 的含义
|
||||
func homeConfigPlayletQueryForConfig(config *homeconfigmod.HomeConfig) (bson.M, bson.D, int64) {
|
||||
categoryID := config.CategoryId
|
||||
if categoryID == 0 && config.RecommendType == 0 && config.Type == homeconfigmod.TypePlaylet {
|
||||
categoryID = homeConfigCategoryID(config.SortType, config.Id)
|
||||
}
|
||||
if config.RecommendType == 0 {
|
||||
filter, sort := homeConfigPlayletQuery(config.SortType, categoryID)
|
||||
return filter, sort, categoryID
|
||||
}
|
||||
filter := homeUpPlayletFilter(nil)
|
||||
field := playletmod.FN.Playlet.UpdatedAt
|
||||
switch config.RecommendType {
|
||||
case 2:
|
||||
field = playletmod.FN.Playlet.CountLike
|
||||
case 3:
|
||||
field = playletmod.FN.Playlet.CountBrowse
|
||||
case 4:
|
||||
field = playletmod.FN.Playlet.HeatValue
|
||||
case 5:
|
||||
if categoryID > 0 {
|
||||
filter[playletmod.FN.Playlet.CategoryIds] = categoryID
|
||||
}
|
||||
field = playletmod.FN.Playlet.RecommendRank
|
||||
}
|
||||
return filter, bson.D{{Key: field, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}, categoryID
|
||||
}
|
||||
|
||||
func findHomeConfigPlayletListForConfig(ctx context.Context, config *homeconfigmod.HomeConfig, limit int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||||
if config.RecommendType == 0 {
|
||||
_, _, categoryID := homeConfigPlayletQueryForConfig(config)
|
||||
return findHomeConfigPlayletList(ctx, config.SortType, categoryID, limit)
|
||||
}
|
||||
filter, sort, categoryID := homeConfigPlayletQueryForConfig(config)
|
||||
if config.RecommendType == 5 {
|
||||
if categoryID <= 0 {
|
||||
return make(playletmod.PlayletList, 0), nil
|
||||
}
|
||||
return playletmod.FindManyPlayletByCategoryRank(ctx, filter, categoryID, 0, limit, playletmod.CategoryRankSort(), playletHomeProjection)
|
||||
}
|
||||
return findHomePlaylets(ctx, filter, sort, limit)
|
||||
}
|
||||
|
||||
func findHomeConfigThemePlayletsForConfig(ctx context.Context, config *homeconfigmod.HomeConfig, skip, limit int64) ([]homeConfigThemePlaylets, playletmod.PlayletList, *srvdef.SrvErr) {
|
||||
if len(config.TopicIds) == 0 {
|
||||
if skip == 0 {
|
||||
return findHomeConfigThemePlaylets(ctx, limit)
|
||||
}
|
||||
groups, _, all, err := findHomeConfigThemePlayletsPage(ctx, skip, limit)
|
||||
return groups, all, err
|
||||
}
|
||||
selected := map[int64]bool{}
|
||||
for _, id := range config.TopicIds {
|
||||
selected[id] = true
|
||||
}
|
||||
result := make([]homeConfigThemePlaylets, 0)
|
||||
all := make(playletmod.PlayletList, 0)
|
||||
var offset int64
|
||||
for _, theme := range playletcache.ListEnabledThemesByPlayletCount(0) {
|
||||
if theme == nil || !selected[theme.Id] {
|
||||
continue
|
||||
}
|
||||
filter := topicDetailPlayletFilter(theme.Id)
|
||||
count, err := playletmod.CountPlaylet(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
if offset < skip {
|
||||
offset++
|
||||
continue
|
||||
}
|
||||
if int64(len(result)) >= limit {
|
||||
break
|
||||
}
|
||||
list, err := findHomePlaylets(ctx, filter, playletTopicDetailSort(topicDetailSortEdit), topicListPlayletLimit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
result = append(result, homeConfigThemePlaylets{Theme: theme, Playlets: list, TotalCount: count})
|
||||
all = append(all, list...)
|
||||
offset++
|
||||
}
|
||||
return result, all, nil
|
||||
}
|
||||
|
||||
func countHomeConfigThemes(ctx context.Context, config *homeconfigmod.HomeConfig) (int64, *srvdef.SrvErr) {
|
||||
if len(config.TopicIds) == 0 {
|
||||
_, count, _, err := findHomeConfigThemePlayletsPage(ctx, 0, 0)
|
||||
return count, err
|
||||
}
|
||||
selected := map[int64]bool{}
|
||||
for _, id := range config.TopicIds {
|
||||
selected[id] = true
|
||||
}
|
||||
var count int64
|
||||
for _, theme := range playletcache.ListEnabledThemesByPlayletCount(0) {
|
||||
if theme == nil || !selected[theme.Id] {
|
||||
continue
|
||||
}
|
||||
filter := topicDetailPlayletFilter(theme.Id)
|
||||
total, err := playletmod.CountPlaylet(ctx, filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if total > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
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[homeconfigmod.FN.HomeConfig.Slug] = slug
|
||||
} else {
|
||||
filter[homeconfigmod.FN.HomeConfig.Id] = req.Id
|
||||
}
|
||||
config, err := homeconfigmod.FindOneHomeConfig(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Type != homeconfigmod.TypePlaylet && config.Type != homeconfigmod.TypeTheme {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "首页配置不存在", "")
|
||||
}
|
||||
pageNum, pageSize := req.PageNum, req.PageSize
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
module := &proto.HomeModuleData{Id: config.Id, Slug: config.Slug, Title: config.Name, Desc: config.Desc, Keyword: homeconfigmod.NormalizeKeywords(config.Keyword), Label: config.Label,
|
||||
Style: config.Style, CardBackgroundImage: withImageDomain(ctx, config.CardBackgroundImage), DataType: 1,
|
||||
RecommendType: config.RecommendType, CategoryId: config.CategoryId, Rank: config.Rank, Limit: homeConfigDisplayLimit(config), SeoInfo: config.SeoInfo,
|
||||
List: make([]*proto.CommonData, 0), TopicList: make([]*proto.HomeConfigThemeData, 0)}
|
||||
if config.Type == homeconfigmod.TypeTheme {
|
||||
module.ModuleType = 2
|
||||
} else {
|
||||
module.ModuleType = 1
|
||||
}
|
||||
resp := &proto.PlayletHomeConfigListResp{Config: module, PageNum: int(pageNum), PageSize: int(pageSize),
|
||||
List: make([]*proto.CommonData, 0), TopicList: make([]*proto.HomeConfigThemeData, 0), Advitise: configser.GetAdvitise()}
|
||||
if !config.Status {
|
||||
return resp, nil
|
||||
}
|
||||
skip := (pageNum - 1) * pageSize
|
||||
if config.Type == homeconfigmod.TypePlaylet {
|
||||
playletFilter, sort, categoryID := homeConfigPlayletQueryForConfig(config)
|
||||
if (config.RecommendType == 5 || (config.RecommendType == 0 && config.SortType == homeconfigmod.SortTypeCategory)) && categoryID <= 0 {
|
||||
return resp, nil
|
||||
}
|
||||
resp.TotalCount, err = playletmod.CountPlaylet(ctx, playletFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var list playletmod.PlayletList
|
||||
if config.RecommendType == 5 || (config.RecommendType == 0 && config.SortType == homeconfigmod.SortTypeCategory) {
|
||||
list, err = playletmod.FindManyPlayletByCategoryRank(ctx, playletFilter, categoryID, skip, pageSize, playletmod.CategoryRankSort(), playletHomeProjection)
|
||||
} else {
|
||||
list, err = findHomePlaylets(ctx, playletFilter, sort, pageSize, options.Find().SetSkip(skip))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
themeNames, err := buildHomeThemeNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagNames, err := buildHomeTagNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.List = playletsToHomeCommonData(ctx, list, themeNames, tagNames)
|
||||
return resp, nil
|
||||
}
|
||||
resp.TotalCount, err = countHomeConfigThemes(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups, all, err := findHomeConfigThemePlayletsForConfig(ctx, config, skip, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
themeNames, err := buildHomeThemeNameMap(ctx, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagNames, err := buildHomeTagNameMap(ctx, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.TopicList = homeConfigThemesToHomeData(ctx, groups, themeNames, tagNames)
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"videoweb/pub/models/homeconfigmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
)
|
||||
|
||||
func TestHomeConfigPlayletQueryForConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config homeconfigmod.HomeConfig
|
||||
field string
|
||||
category int64
|
||||
recommended bool
|
||||
}{
|
||||
{name: "legacy recommendation", config: homeconfigmod.HomeConfig{SortType: homeconfigmod.SortTypeRecommend}, field: playletmod.FN.Playlet.RecommendRank, recommended: true},
|
||||
{name: "new likes", config: homeconfigmod.HomeConfig{RecommendType: 2}, field: playletmod.FN.Playlet.CountLike},
|
||||
{name: "new category", config: homeconfigmod.HomeConfig{RecommendType: 5, CategoryId: 42}, field: playletmod.FN.Playlet.RecommendRank, category: 42},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter, sort, category := homeConfigPlayletQueryForConfig(&tt.config)
|
||||
if category != tt.category {
|
||||
t.Fatalf("category = %d, want %d", category, tt.category)
|
||||
}
|
||||
if len(sort) == 0 || sort[0].Key != tt.field {
|
||||
t.Fatalf("sort = %v, want %s", sort, tt.field)
|
||||
}
|
||||
if _, ok := filter[playletmod.FN.Playlet.IsRecommend]; ok != tt.recommended {
|
||||
t.Fatalf("recommend filter = %v, want %v", ok, tt.recommended)
|
||||
}
|
||||
if tt.category > 0 && filter[playletmod.FN.Playlet.CategoryIds] != tt.category {
|
||||
t.Fatalf("category filter = %v", filter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletlikemod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type playletLikePage struct {
|
||||
Total []struct {
|
||||
Count int64 `bson:"count"`
|
||||
} `bson:"total"`
|
||||
List playletlikemod.PlayletLikeList `bson:"list"`
|
||||
}
|
||||
|
||||
// LikeList 按点赞时间倒序分页查询当前用户仍上架的短剧。
|
||||
func LikeList(ctx context.Context, userId int64, req *proto.PlayletLikeListReq) (*proto.PlayletLikeListResp, *srvdef.SrvErr) {
|
||||
if userId <= 0 {
|
||||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||||
}
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||||
resp := &proto.PlayletLikeListResp{
|
||||
PageNum: int(pageNum), PageSize: int(pageSize), List: make([]proto.PlayletLikeData, 0),
|
||||
}
|
||||
|
||||
// 在分页之前过滤下架或已删除的短剧,避免总数和当前页不一致。
|
||||
pipeline := []bson.M{
|
||||
{"$match": bson.M{playletlikemod.FN.PlayletLike.UserId: userId}},
|
||||
{"$sort": bson.D{
|
||||
{Key: playletlikemod.FN.PlayletLike.CreatedAt, Value: -1},
|
||||
{Key: playletlikemod.FN.PlayletLike.Id, Value: -1},
|
||||
}},
|
||||
{"$lookup": bson.M{
|
||||
"from": playletmod.TN.Playlet, "localField": playletlikemod.FN.PlayletLike.PlayletId,
|
||||
"foreignField": playletmod.FN.Playlet.Id, "as": "playlet",
|
||||
}},
|
||||
{"$unwind": "$playlet"},
|
||||
{"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
|
||||
{"$facet": bson.M{
|
||||
"total": bson.A{bson.M{"$count": "count"}},
|
||||
"list": bson.A{
|
||||
bson.M{"$skip": (pageNum - 1) * pageSize},
|
||||
bson.M{"$limit": pageSize},
|
||||
bson.M{"$project": bson.M{
|
||||
playletlikemod.FN.PlayletLike.Id: 1, playletlikemod.FN.PlayletLike.PlayletId: 1,
|
||||
playletlikemod.FN.PlayletLike.CreatedAt: 1,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
var pages []playletLikePage
|
||||
if err := playletlikemod.AggregatePlayletLike(ctx, &pages, pipeline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pages) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
page := pages[0]
|
||||
if len(page.Total) > 0 {
|
||||
resp.TotalCount = page.Total[0].Count
|
||||
}
|
||||
if len(page.List) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ids := make([]int64, 0, len(page.List))
|
||||
for _, like := range page.List {
|
||||
if like != nil {
|
||||
ids = append(ids, like.PlayletId)
|
||||
}
|
||||
}
|
||||
playlets, err := playletmod.FindManyPlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||||
}), options.Find().SetProjection(playletHomeProjection))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
themeNames, err := buildHomeThemeNameMap(ctx, playlets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagNames, err := buildHomeTagNameMap(ctx, playlets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataByID := make(map[int64]*proto.CommonData, len(playlets))
|
||||
for _, data := range playletsToHomeCommonData(ctx, playlets, themeNames, tagNames) {
|
||||
dataByID[data.Id] = data
|
||||
}
|
||||
for _, like := range page.List {
|
||||
if like == nil {
|
||||
continue
|
||||
}
|
||||
if data := dataByID[like.PlayletId]; data != nil {
|
||||
resp.List = append(resp.List, proto.PlayletLikeData{
|
||||
CommonData: *data, IsLike: operateStatusYes, LikedAt: like.CreatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const operateRandomModulus int64 = 2147483647
|
||||
|
||||
func operateListFilter(typ int64) bson.M {
|
||||
filter := homeUpPlayletFilter(nil)
|
||||
if typ == operateListOriginal {
|
||||
filter[playletmod.FN.Playlet.IsRecommend] = true
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func operateListSort(typ int64) bson.D {
|
||||
if typ == operateListHot {
|
||||
return bson.D{{Key: playletmod.FN.Playlet.HeatValue, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
|
||||
}
|
||||
return playletRecentlyUpdatedSort()
|
||||
}
|
||||
|
||||
// findRandomOperatePlaylets 按每日固定的伪随机顺序分页查询短剧
|
||||
func findRandomOperatePlaylets(ctx context.Context, filter bson.M, skip, limit int64, now time.Time) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||||
var rows []struct {
|
||||
Id int64 `bson:"_id"`
|
||||
}
|
||||
if err := playletmod.AggregatePlaylet(ctx, &rows, operateRandomPipeline(filter, skip, limit, now)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, row.Id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return make(playletmod.PlayletList, 0), nil
|
||||
}
|
||||
queryFilter := bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}
|
||||
for key, value := range filter {
|
||||
queryFilter[key] = value
|
||||
}
|
||||
list, err := playletmod.FindManyPlaylet(ctx, queryFilter, options.Find().SetProjection(playletHomeProjection))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := make(map[int64]*playletmod.Playlet, len(list))
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
byID[item.Id] = item
|
||||
}
|
||||
}
|
||||
ordered := make(playletmod.PlayletList, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if item := byID[id]; item != nil {
|
||||
ordered = append(ordered, item)
|
||||
}
|
||||
}
|
||||
return ordered, nil
|
||||
}
|
||||
|
||||
func operateRandomPipeline(filter bson.M, skip, limit int64, now time.Time) []bson.M {
|
||||
day := (now.Unix() + 8*60*60) / (24 * 60 * 60)
|
||||
factor := int64(48271) + day%997*2767
|
||||
idMod := bson.M{"$mod": bson.A{"$_id", operateRandomModulus}}
|
||||
score := bson.M{"$mod": bson.A{bson.M{"$multiply": bson.A{idMod, factor}}, operateRandomModulus}}
|
||||
return []bson.M{
|
||||
{"$match": filter},
|
||||
{"$project": bson.M{"_id": 1}},
|
||||
{"$addFields": bson.M{"randomScore": score}},
|
||||
{"$sort": bson.D{{Key: "randomScore", Value: 1}, {Key: "_id", Value: 1}}},
|
||||
{"$skip": skip},
|
||||
{"$limit": limit},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
"videoweb/pub/models/playletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func TestOperateListFiltersAndSorts(t *testing.T) {
|
||||
for _, typ := range []int64{operateListLatest, operateListHot, operateListOriginal, operateListRandom} {
|
||||
filter := operateListFilter(typ)
|
||||
if filter[playletmod.FN.Playlet.Status] != playletmod.StatusUp {
|
||||
t.Fatalf("type %d must contain the up status filter", typ)
|
||||
}
|
||||
_, recommend := filter[playletmod.FN.Playlet.IsRecommend]
|
||||
if recommend != (typ == operateListOriginal) {
|
||||
t.Fatalf("type %d recommend filter = %v", typ, recommend)
|
||||
}
|
||||
}
|
||||
if got := operateListSort(operateListLatest)[0].Key; got != playletmod.FN.Playlet.UpdatedAt {
|
||||
t.Fatalf("latest sort = %s", got)
|
||||
}
|
||||
if got := operateListSort(operateListHot)[0].Key; got != playletmod.FN.Playlet.HeatValue {
|
||||
t.Fatalf("hot sort = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperateRandomPipelineIsStableWithinDay(t *testing.T) {
|
||||
filter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
|
||||
day := time.Date(2026, 10, 24, 1, 0, 0, 0, time.UTC)
|
||||
first := operateRandomPipeline(filter, 20, 10, day)
|
||||
second := operateRandomPipeline(filter, 20, 10, day.Add(time.Hour))
|
||||
if !reflect.DeepEqual(first, second) {
|
||||
t.Fatal("random order changed within one China Standard Time day")
|
||||
}
|
||||
third := operateRandomPipeline(filter, 20, 10, day.Add(24*time.Hour))
|
||||
if reflect.DeepEqual(first, third) {
|
||||
t.Fatal("random order did not change on the next China Standard Time day")
|
||||
}
|
||||
if first[4]["$skip"] != int64(20) || first[5]["$limit"] != int64(10) {
|
||||
t.Fatalf("pagination stages = %v", first)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"videoweb/app/service/playletcache"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playlettagmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// resolvePlayletID 解析短剧ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
|
||||
func resolvePlayletID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
|
||||
slug = strings.TrimSpace(slug)
|
||||
if slug != "" {
|
||||
item, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Slug: slug,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return item.Id, nil
|
||||
}
|
||||
if required && id <= 0 {
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// resolveThemeID 解析题材ID:slug 非空时先按 slug 查 ID,否则使用传入 ID(0 表示不按题材筛选)。
|
||||
func resolveThemeID(ctx context.Context, id int64, slug string) (int64, *srvdef.SrvErr) {
|
||||
slug = strings.TrimSpace(slug)
|
||||
if slug == "" {
|
||||
return id, nil
|
||||
}
|
||||
if item := playletcache.GetThemeBySlug(slug); item != nil {
|
||||
return item.Id, nil
|
||||
}
|
||||
theme, err := playletthememod.FindOnePlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Slug: slug,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return theme.Id, nil
|
||||
}
|
||||
|
||||
// resolveTagID 解析标签ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
|
||||
func resolveTagID(ctx context.Context, id int64, slug string) (int64, *srvdef.SrvErr) {
|
||||
slug = strings.TrimSpace(slug)
|
||||
if slug != "" {
|
||||
if item := playletcache.GetTagBySlug(slug); item != nil {
|
||||
return item.Id, nil
|
||||
}
|
||||
tag, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{
|
||||
playlettagmod.FN.PlayletTag.Slug: slug,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playlettagmod.FN.PlayletTag.Id, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.Id, nil
|
||||
}
|
||||
if id <= 0 {
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "标签ID不能为空")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/pub/models/playlettopicgroupmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func enabledTopicGroups(ctx context.Context) (playlettopicgroupmod.PlayletTopicGroupList, *srvdef.SrvErr) {
|
||||
return playlettopicgroupmod.FindMany(ctx, bson.M{"status": true})
|
||||
}
|
||||
|
||||
type topicPlayletCount struct {
|
||||
Id int64 `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
|
||||
// topicGroupPage 分页查询区块下有已上架短剧的专题
|
||||
func topicGroupPage(ctx context.Context, group *playlettopicgroupmod.PlayletTopicGroup, skip, limit int64) (playletthememod.PlayletThemeList, map[int64]int64, int64, *srvdef.SrvErr) {
|
||||
counts := make(map[int64]int64)
|
||||
if len(group.TopicIds) == 0 {
|
||||
return playletthememod.PlayletThemeList{}, counts, 0, nil
|
||||
}
|
||||
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": group.TopicIds},
|
||||
playletthememod.FN.PlayletTheme.Status: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
byID := make(map[int64]*playletthememod.PlayletTheme, len(themes))
|
||||
for _, theme := range themes {
|
||||
if theme != nil {
|
||||
byID[theme.Id] = theme
|
||||
}
|
||||
}
|
||||
rows := make([]topicPlayletCount, 0)
|
||||
err = playletmod.AggregatePlaylet(ctx, &rows, []bson.M{
|
||||
{"$match": bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp, playletmod.FN.Playlet.ThemeIds: bson.M{"$in": group.TopicIds}}},
|
||||
{"$unwind": "$" + playletmod.FN.Playlet.ThemeIds},
|
||||
{"$match": bson.M{playletmod.FN.Playlet.ThemeIds: bson.M{"$in": group.TopicIds}}},
|
||||
{"$group": bson.M{"_id": "$" + playletmod.FN.Playlet.ThemeIds, "count": bson.M{"$sum": 1}}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
counts[row.Id] = row.Count
|
||||
}
|
||||
available := make(playletthememod.PlayletThemeList, 0, len(themes))
|
||||
seen := make(map[int64]bool, len(group.TopicIds))
|
||||
for _, id := range group.TopicIds {
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
if theme := byID[id]; theme != nil && counts[id] > 0 {
|
||||
available = append(available, theme)
|
||||
}
|
||||
}
|
||||
total := int64(len(available))
|
||||
if skip >= total {
|
||||
return playletthememod.PlayletThemeList{}, counts, total, nil
|
||||
}
|
||||
end := skip + limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return available[skip:end], counts, total, nil
|
||||
}
|
||||
|
||||
// topicGroupPageData 组装区块当前页专题及单部短剧预览
|
||||
func topicGroupPageData(ctx context.Context, group *playlettopicgroupmod.PlayletTopicGroup, skip, limit int64) (*proto.PlayletTopicGroupPageItem, *srvdef.SrvErr) {
|
||||
themes, counts, totalCount, err := topicGroupPage(ctx, group, skip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page := &proto.PlayletTopicGroupPageItem{
|
||||
Id: group.Id, Name: group.Name, Desc: group.Desc, Rank: group.Rank,
|
||||
TotalCount: totalCount, List: make([]*proto.PlayletTopicPageItem, 0, len(themes)),
|
||||
}
|
||||
if len(themes) == 0 {
|
||||
return page, nil
|
||||
}
|
||||
previewByTheme := make(map[int64]playletmod.PlayletList, len(themes))
|
||||
previews := make(playletmod.PlayletList, 0, len(themes))
|
||||
for _, theme := range themes {
|
||||
filter, sort := playletTopicPagePlayletQuery(0, theme.Id)
|
||||
list, err := findHomePlaylets(ctx, filter, sort, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previewByTheme[theme.Id] = list
|
||||
previews = append(previews, list...)
|
||||
}
|
||||
themeNameMap, err := buildHomeThemeNameMap(ctx, previews)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagNameMap, err := buildHomeTagNameMap(ctx, previews)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, theme := range themes {
|
||||
preview := playletsToHomeCommonData(ctx, previewByTheme[theme.Id], themeNameMap, tagNameMap)
|
||||
covers := make([]string, 0, len(preview))
|
||||
if len(preview) > 0 {
|
||||
cover := preview[0].HorizontalCover
|
||||
if cover == "" {
|
||||
cover = preview[0].Cover
|
||||
}
|
||||
if cover != "" {
|
||||
covers = append(covers, cover)
|
||||
}
|
||||
}
|
||||
page.List = append(page.List, &proto.PlayletTopicPageItem{
|
||||
Id: theme.Id, Name: theme.Name, Slug: theme.Slug,
|
||||
Desc: strings.TrimSpace(theme.SeoInfo.Description),
|
||||
Icon: withImageDomain(ctx, theme.Icon), Rank: theme.Rank,
|
||||
PlayletCount: counts[theme.Id], Covers: covers, List: preview,
|
||||
SeoInfo: buildPageSeoInfoFromValue(theme.SeoInfo, theme.Name, "", buildPlayletThemeListCanonicalURL(theme.Slug, theme.Id)),
|
||||
})
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
Reference in New Issue
Block a user