@@ -0,0 +1,341 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/mediacontentser"
|
||||
"91porn-server/app/service/mediaser"
|
||||
recommendqueue "91porn-server/common/shortrecommend"
|
||||
"91porn-server/models/cache/mediadata"
|
||||
"91porn-server/models/v/media_buy_record_mod"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const dramaFeedCandidateLimit = 500
|
||||
|
||||
func GetChannelConfig(now time.Time) (ChannelConfig, error) {
|
||||
modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Drama)
|
||||
if err != nil {
|
||||
return ChannelConfig{}, err
|
||||
}
|
||||
active := make([]moduleconfmod.ModuleConf, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
if module.IsActiveAt(now) {
|
||||
active = append(active, module)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(active, func(i, j int) bool { return active[i].SortNum > active[j].SortNum })
|
||||
var aiModuleID, hotModuleID primitive.ObjectID
|
||||
for _, module := range active {
|
||||
name := strings.ToLower(strings.TrimSpace(module.ModuleName))
|
||||
switch {
|
||||
case strings.Contains(name, "热门") || strings.Contains(name, "hot"):
|
||||
if hotModuleID.IsZero() {
|
||||
hotModuleID = module.ID
|
||||
}
|
||||
case strings.Contains(name, "ai"):
|
||||
if aiModuleID.IsZero() {
|
||||
aiModuleID = module.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
if aiModuleID.IsZero() && len(active) > 0 {
|
||||
aiModuleID = active[0].ID
|
||||
}
|
||||
if hotModuleID.IsZero() && len(active) > 1 {
|
||||
hotModuleID = active[1].ID
|
||||
}
|
||||
return ChannelConfig{
|
||||
BottomTabTitle: "短剧",
|
||||
DefaultTab: TabAIDrama,
|
||||
AdEnabled: false,
|
||||
Tabs: []ChannelTab{
|
||||
{Key: TabLive, Title: "直播", Enabled: true, Sort: 1},
|
||||
{Key: TabDouyin, Title: "抖音", Enabled: true, Sort: 2},
|
||||
{Key: TabAIDrama, Title: "AI短剧", Enabled: !aiModuleID.IsZero(), Sort: 3, ModuleID: objectIDString(aiModuleID)},
|
||||
{Key: TabHotDrama, Title: "热门短剧", Enabled: !hotModuleID.IsZero(), Sort: 4, ModuleID: objectIDString(hotModuleID)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func objectIDString(id primitive.ObjectID) string {
|
||||
if id.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return id.Hex()
|
||||
}
|
||||
|
||||
func GetList(uid uint64, req ListRequest) (ListResponse, error) {
|
||||
filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}
|
||||
sortBy := dramaListSort(req.SortType)
|
||||
total, err := mediamod.QueryAllCount(filter)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
list, hasNext, err := mediadata.GetListFromCache(filter, req.Skip64(), req.Limit64(), sortBy)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
return ListResponse{Total: total, HasNext: hasNext, List: mediaser.FillMedias(list, uid, true)}, nil
|
||||
}
|
||||
|
||||
func dramaListSort(sortType int) bson.D {
|
||||
sortBy := bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
switch sortType {
|
||||
case 2:
|
||||
sortBy = bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
case 3:
|
||||
sortBy = bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
case 4:
|
||||
sortBy = bson.D{{Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
}
|
||||
return sortBy
|
||||
}
|
||||
|
||||
func GetFeed(ctx context.Context, uid uint64, pageSize uint64, now time.Time) (FeedResponse, error) {
|
||||
filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}
|
||||
candidates, _, err := mediadata.GetListFromCache(
|
||||
filter, 0, dramaFeedCandidateLimit,
|
||||
bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}},
|
||||
)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
ordered := orderFeedCandidates(candidates, now)
|
||||
if len(ordered) == 0 {
|
||||
return FeedResponse{QueueVersion: "drama-" + recommendqueue.VersionAt(now), List: []FeedItem{}}, nil
|
||||
}
|
||||
queueIDs := make([]string, 0, len(ordered))
|
||||
mediaByID := make(map[string]*mediamod.Media, len(ordered))
|
||||
for _, media := range ordered {
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
id := media.ID.Hex()
|
||||
queueIDs = append(queueIDs, id)
|
||||
mediaByID[id] = media
|
||||
}
|
||||
queueVersion := dramaFeedQueueVersion(now, queueIDs)
|
||||
reservation, err := reserveDramaFeedPage(
|
||||
ctx, appg.Redis, uid, int(pageSize)*dramaFeedScanMultiplier, queueVersion, queueIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
abortCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = abortDramaFeedPage(abortCtx, appg.Redis, uid, reservation)
|
||||
}
|
||||
}()
|
||||
reservedMedias := make([]*mediamod.Media, 0, len(reservation.IDs))
|
||||
for _, id := range reservation.IDs {
|
||||
if media := mediaByID[id]; media != nil {
|
||||
reservedMedias = append(reservedMedias, media)
|
||||
}
|
||||
}
|
||||
hasCard, err := mediacontentser.ActiveDramaCardStatus(uid)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
mediaResponses := mediaser.FillMedias(reservedMedias, uid, true, hasCard)
|
||||
episodes, err := firstEpisodeMap(uid, reservedMedias, hasCard)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
responseByID := make(map[string]*mediamod.AppMediaBase, len(mediaResponses))
|
||||
for _, media := range mediaResponses {
|
||||
if media != nil {
|
||||
responseByID[media.ID.Hex()] = media
|
||||
}
|
||||
}
|
||||
items := make([]FeedItem, 0, pageSize)
|
||||
scanned := 0
|
||||
for _, id := range reservation.IDs {
|
||||
scanned++
|
||||
media := responseByID[id]
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
episode, ok := episodes[media.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, FeedItem{Media: media, Content: episode})
|
||||
if len(items) >= int(pageSize) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := commitDramaFeedPage(ctx, appg.Redis, uid, reservation, scanned); err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
committed = true
|
||||
return FeedResponse{
|
||||
QueueVersion: "drama-" + reservation.Version,
|
||||
HasNext: reservation.Length > len(items),
|
||||
List: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dramaFeedQueueVersion 将当天候选集快照写入版本。上架、下架导致候选集变化时,
|
||||
// 新请求会切换到独立队列;ID 排序后再计算,避免仅推荐顺序变化造成队列频繁重建。
|
||||
func dramaFeedQueueVersion(now time.Time, queueIDs []string) string {
|
||||
ids := append([]string(nil), queueIDs...)
|
||||
sort.Strings(ids)
|
||||
h := fnv.New64a()
|
||||
for _, id := range ids {
|
||||
_, _ = h.Write([]byte(id))
|
||||
_, _ = h.Write([]byte{0})
|
||||
}
|
||||
return recommendqueue.VersionWithRevision(now, fmt.Sprintf("%016x", h.Sum64()))
|
||||
}
|
||||
|
||||
func orderFeedCandidates(candidates []*mediamod.Media, now time.Time) []*mediamod.Media {
|
||||
recommended := make([]*mediamod.Media, 0)
|
||||
regular := make([]vidmod.RecommendCandidate, 0)
|
||||
mediaByID := make(map[primitive.ObjectID]*mediamod.Media, len(candidates))
|
||||
for _, media := range candidates {
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
mediaByID[media.ID] = media
|
||||
candidate := vidmod.RecommendCandidate{
|
||||
ID: media.ID, ReviewAt: dramaReviewAt(media), RecommendScore: dramaInteractionScore(media),
|
||||
}
|
||||
if media.SortCode > 0 || !media.Choice.IsZero() {
|
||||
recommended = append(recommended, media)
|
||||
continue
|
||||
}
|
||||
regular = append(regular, candidate)
|
||||
}
|
||||
rng := rand.New(rand.NewSource(dramaQueueSeed(now)))
|
||||
rng.Shuffle(len(recommended), func(i, j int) {
|
||||
recommended[i], recommended[j] = recommended[j], recommended[i]
|
||||
})
|
||||
// 直接复用91P短视频队列的17条互动高分+3条24小时新内容组装算法。
|
||||
regularIDs := recommendqueue.Assemble(regular, now)
|
||||
ordered := make([]*mediamod.Media, 0, len(candidates))
|
||||
ordered = append(ordered, recommended...)
|
||||
ordered = appendDramaIDs(ordered, regularIDs, mediaByID)
|
||||
return ordered
|
||||
}
|
||||
|
||||
func dramaQueueSeed(now time.Time) int64 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte("drama:" + recommendqueue.VersionAt(now)))
|
||||
return int64(h.Sum64())
|
||||
}
|
||||
|
||||
func dramaInteractionScore(media *mediamod.Media) int64 {
|
||||
if media == nil {
|
||||
return 0
|
||||
}
|
||||
return vidmod.CalculateRecommendScore(media.CountLike, media.CountCollect, media.CountComment, media.CountShare)
|
||||
}
|
||||
|
||||
func dramaReviewAt(media *mediamod.Media) time.Time {
|
||||
if media != nil && !media.LatestPublishedAt.IsZero() {
|
||||
return media.LatestPublishedAt
|
||||
}
|
||||
if media == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return media.CreatedAt
|
||||
}
|
||||
|
||||
func appendDramaIDs(
|
||||
ordered []*mediamod.Media,
|
||||
ids []string,
|
||||
mediaByID map[primitive.ObjectID]*mediamod.Media,
|
||||
) []*mediamod.Media {
|
||||
for _, rawID := range ids {
|
||||
id, err := primitive.ObjectIDFromHex(rawID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if media := mediaByID[id]; media != nil {
|
||||
ordered = append(ordered, media)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func firstEpisodeMap(uid uint64, medias []*mediamod.Media, hasCard bool) (map[primitive.ObjectID]mediacontentser.AppMediaContent, error) {
|
||||
result := make(map[primitive.ObjectID]mediacontentser.AppMediaContent, len(medias))
|
||||
if len(medias) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
mediaIDs := make([]primitive.ObjectID, 0, len(medias))
|
||||
mediaMap := make(map[primitive.ObjectID]*mediamod.Media, len(medias))
|
||||
for _, media := range medias {
|
||||
if media != nil {
|
||||
mediaIDs = append(mediaIDs, media.ID)
|
||||
mediaMap[media.ID] = media
|
||||
}
|
||||
}
|
||||
contents, err := mediacontentmod.QueryAllList(
|
||||
bson.M{"mediaId": bson.M{"$in": mediaIDs}, "mediaType": mediamod.MediaTypeDrama, "episodeNumber": 1, "isActive": true, "isDelete": false},
|
||||
options.Find().SetSort(bson.D{{Key: "createdAt", Value: 1}}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentIDs := make([]primitive.ObjectID, 0, len(contents))
|
||||
for _, content := range contents {
|
||||
if content != nil {
|
||||
contentIDs = append(contentIDs, content.ID)
|
||||
}
|
||||
}
|
||||
bought, err := media_buy_record_mod.IsContentBuyBatch(uid, contentIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, content := range contents {
|
||||
if content == nil || result[content.MediaID].ID != primitive.NilObjectID {
|
||||
continue
|
||||
}
|
||||
media := mediaMap[content.MediaID]
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
isFree, canPlay, accessType := mediacontentser.ResolveDramaAccess(*content, *media, bought[content.ID], hasCard)
|
||||
name := strings.TrimSpace(content.Name)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("第%d集", content.EpisodeNumber)
|
||||
}
|
||||
item := mediacontentser.AppMediaContent{
|
||||
ID: content.ID, MediaID: content.MediaID, MediaType: content.MediaType,
|
||||
EpisodeNumber: content.EpisodeNumber, ListenPermission: content.ListenPermission,
|
||||
VideoUrl: content.VideoUrl, H265Url: appg.H265URLForApp(content.H265Url), AudioUrl: content.AudioUrl,
|
||||
PlayTime: content.PlayTime, Price: content.Price, Name: name, Cover: content.Cover,
|
||||
HasBuy: bought[content.ID], IsFree: isFree, HasDramaCard: hasCard,
|
||||
CanPlay: canPlay, AccessType: accessType, Ratio: content.Ratio,
|
||||
PreviewEnabled: content.PreviewEnabled, PreviewStart: content.PreviewStart, PreviewSeconds: content.PreviewSeconds,
|
||||
CreatedAt: content.CreatedAt, UpdateTime: content.UpdateTime,
|
||||
}
|
||||
if !item.CanPlay {
|
||||
if item.PreviewEnabled {
|
||||
item.PreviewVideoUrl, item.PreviewH265Url = item.VideoUrl, item.H265Url
|
||||
}
|
||||
item.VideoUrl, item.H265Url, item.AudioUrl = "", "", ""
|
||||
}
|
||||
result[content.MediaID] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user