Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

1303 lines
39 KiB
Go
Executable File

package mediaser
import (
"91porn-server/app/appg"
"91porn-server/app/service/mediatagser"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/elastic"
"91porn-server/common/log"
"91porn-server/common/timeutil"
topser "91porn-server/common/top"
topasist "91porn-server/common/top/asistant"
"91porn-server/common/top/dailytop"
"91porn-server/common/top/monthtop"
"91porn-server/common/top/weektop"
"91porn-server/models/cache/mediadata"
"91porn-server/models/cache/mediatagdata"
"91porn-server/models/commod"
"91porn-server/models/s/sectionstatmod"
"91porn-server/models/v/likemod"
"91porn-server/models/v/media_buy_record_mod"
"91porn-server/models/v/mediabookshelfmod"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/mediatagdimensionmod"
"91porn-server/models/v/mediatagmod"
"91porn-server/models/v/modulesectionmod"
"fmt"
"github.com/golang-module/carbon/v2"
"github.com/vmihailenco/msgpack/v5"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"strconv"
"sync"
"time"
)
type AppQueryListReq struct {
commod.Page
}
type AppListRes struct {
HasNext bool `json:"hasNext"`
List []*mediamod.AppMediaBase `json:"list"`
}
// GetList 获取列表
func (p *AppQueryListReq) GetList() AppListRes {
var res AppListRes
var err error
filter := bson.M{}
sort := bson.D{{"_id", -1}}
// 获取列表
var data []*mediamod.Media
data, res.HasNext, err = mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
if err != nil {
return res
}
// 组装返回数据
for _, item := range data {
tmp := &mediamod.AppMediaBase{
ID: item.ID,
SortCode: item.SortCode,
Status: item.Status,
Title: item.Title,
Author: item.Anchor,
HorizontalCover: item.HorizontalCover,
VerticalCover: item.VerticalCover,
Tags: item.Tags,
Summary: item.Summary,
TotalEpisode: item.TotalEpisode,
MediaType: item.MediaType,
MediaSubType: item.MediaSubType,
Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide,
Permission: item.Permission,
Price: item.Price,
Direction: item.Direction,
FreeTime: item.FreeTime,
CountComment: item.CountComment,
CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse,
CountCollect: item.CountCollect,
CountLike: item.CountLike,
CountView: item.CountView,
Number: item.Number,
CreatedAt: item.CreatedAt,
UpdateTime: item.UpdateTime,
}
res.List = append(res.List, tmp)
}
return res
}
type AppQueryInfoReq struct {
ID string `json:"id" form:"id"` // id
}
// GetInfo 获取详情
func (p *AppQueryInfoReq) GetInfo(uid uint64) (resp *mediamod.AppMediaBase, err error) {
oid, err := primitive.ObjectIDFromHex(p.ID)
if err != nil {
return resp, err
}
var item mediamod.Media
item, err = mediadata.GetInfoFromCache(oid)
if err != nil {
log.Error("AppQueryInfoReq.GetInfo mediadata.GetInfoFromCache fail", log.Any("mediaId", p.ID), log.E(err))
return resp, err
}
if item.ID.IsZero() {
return resp, nil
}
if item.MediaType == mediamod.MediaTypeDrama && (item.Status != 1 || item.IsDelete) {
return nil, fmt.Errorf("drama is inactive")
}
res := FillMedias([]*mediamod.Media{&item}, uid, true)
resp = res[0]
// 累加浏览数
common.Go(func() {
// 增加父级的浏览数量
IncrCountBrowse(item.ID)
topasist.Incr(topser.TypeMedia(item.MediaType), item.ID.Hex(), 1)
})
return resp, nil
}
// GetUserStatus 获取 用户状态信息 只跟media这张表有关系
func GetUserStatus(uid uint64, mediaIds []primitive.ObjectID) map[primitive.ObjectID]mediamod.MediaStatus {
mPay := make(map[primitive.ObjectID]bool)
hasAddBookshelf := make(map[primitive.ObjectID]bool)
mLike := make(map[primitive.ObjectID]bool)
var wg sync.WaitGroup
wg.Add(3)
common.Go(func() {
defer wg.Done()
mPay, _ = media_buy_record_mod.IsWholeBuyBatch(uid, mediaIds)
})
common.Go(func() {
defer wg.Done()
mLike, _ = likemod.IsLikeByType(uid, []string{constant.LikeTypeText, constant.LikeTypeComics, constant.LikeTypeCartoon, constant.LikeTypeDrama}, mediaIds)
})
common.Go(func() {
defer wg.Done()
hasAddBookshelf, _ = mediabookshelfmod.HasAddBookshelf(uid, mediaIds)
})
wg.Wait()
m := map[primitive.ObjectID]mediamod.MediaStatus{}
for _, v := range mediaIds {
info := mediamod.MediaStatus{
HasPaid: mPay[v],
HasCollected: hasAddBookshelf[v],
HasLiked: mLike[v],
}
m[v] = info
}
return m
}
// GetLibrary 获取片库
func GetLibrary() (res []mediamod.AppLibrary, err error) {
var resp []mediamod.AppLibrary
str, err := appg.Redis.Get(redisconst.MediaLibraryInfo)
if err != nil {
log.Warn(fmt.Sprintf("缓存获取动漫片库信息异常:%v", err))
}
if str != nil {
if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil {
return resp, nil
}
log.Warn(fmt.Sprintf("解析预售缓存数据异常:%v", err))
}
resp = append(resp, mediamod.AppLibrary{
Key: Video,
Name: "动漫",
}, mediamod.AppLibrary{
Key: Image,
Name: "漫画",
})
for i, d := range resp {
if d.Key == Video {
resp[i].Kind = append(resp[i].Kind, mediamod.KindInfo{
Key: "1",
Name: "哩番",
Cover: "cf230705/image/lm/34n/1la/2ck/7064ad102298311797d9f43b323a2ea4.png",
}, mediamod.KindInfo{
Key: "2",
Name: "3D",
Cover: "cf230705/image/18b/2zw/1dp/2wu/3b90ad609435c0037f7a8a90ed8e2de2.png",
}, mediamod.KindInfo{
Key: "3",
Name: "同人动画",
Cover: "cf230705/image/14q/5y/19x/p9/3c48b94311b361f2dcdb006200208751.png",
})
}
if d.Key == Image {
resp[i].Kind = append(resp[i].Kind, mediamod.KindInfo{
Key: "4",
Name: "单行本",
Cover: "cf230705/image/2a4/1kk/2hi/2xg/b830c4997f21ad4351ab0880fd2bc1be.png",
}, mediamod.KindInfo{
Key: "5",
Name: "连载漫画",
Cover: "cf230705/image/p2/1rq/2c3/nx/ce44dfc5406ef4d20cef04a683970f4c.png",
}, mediamod.KindInfo{
Key: "6",
Name: "韩漫",
Cover: "cf230705/image/1ol/26f/lb/2rq/872dab8f0ccd3c4eba6cba5171630112.png",
})
}
}
// 获取 动漫、漫画纬度
filter := bson.M{"status": 1, "isDelete": false}
op := options.Find().SetSort(bson.D{{"sortCode", -1}})
tagDimensions, err := mediatagdimensionmod.QueryAllList(filter, op)
if err != nil {
return resp, nil
}
if len(tagDimensions) > 0 {
for i, m := range resp {
for _, d := range tagDimensions {
if m.Key == "video" && d.Type == 1 {
for j := range resp[i].Kind {
resp[i].Kind[j].Dimension = append(resp[i].Kind[j].Dimension, mediamod.DimensionInfo{
Key: d.ID.Hex(),
Name: d.Name,
})
}
}
if m.Key == "image" && d.Type == 2 {
for j := range resp[i].Kind {
resp[i].Kind[j].Dimension = append(resp[i].Kind[j].Dimension, mediamod.DimensionInfo{
Key: d.ID.Hex(),
Name: d.Name,
})
}
}
}
}
}
// 获取所有推荐标签
tagFilter := bson.M{"active": true, "isDelete": false, "isDiscovery": true}
tagOp := options.Find().SetSort(bson.D{{"sort", -1}})
mediaTags, err := mediatagmod.QueryAllList(tagFilter, tagOp)
if err != nil {
return resp, nil
}
if len(mediaTags) > 0 {
for i, m := range resp {
for _, t := range mediaTags {
if m.Key == "video" && t.Type == 1 && (t.Kind == 1 || t.Kind == 2 || t.Kind == 3) {
for j := range resp[i].Kind {
for d, k := range resp[i].Kind[j].Dimension {
kindStr := strconv.Itoa(t.Kind)
if t.Dimension == k.Name && resp[i].Kind[j].Key == kindStr {
resp[i].Kind[j].Dimension[d].Tag = append(resp[i].Kind[j].Dimension[d].Tag, mediamod.TagInfo{
ID: t.ID.Hex(),
Name: t.Name,
})
}
}
}
}
if m.Key == "image" && t.Type == 2 && (t.Kind == 4 || t.Kind == 5 || t.Kind == 6) {
for j := range resp[i].Kind {
for d, k := range resp[i].Kind[j].Dimension {
kindStr := strconv.Itoa(t.Kind)
if t.Dimension == k.Name && resp[i].Kind[j].Key == kindStr {
resp[i].Kind[j].Dimension[d].Tag = append(resp[i].Kind[j].Dimension[d].Tag, mediamod.TagInfo{
ID: t.ID.Hex(),
Name: t.Name,
})
}
}
}
}
}
}
}
common.Go(func() {
bytes, _ := msgpack.Marshal(resp)
if err = appg.Redis.Set(redisconst.MediaLibraryInfo, bytes, 11*time.Minute); err != nil {
log.Warn(fmt.Sprintf("保存预售缓存数据异常:%v", err))
}
})
return resp, nil
}
func LibrarySearch(in mediamod.AppLibraryReq) (data mediamod.AppElasticSearchLibraryResponse, err error) {
var resp mediamod.AppElasticSearchLibraryResponse
from := int64((in.PageNumber - 1) * (in.PageSize))
size := int64(in.PageSize + 1)
must := elastic.A{
{"term": elastic.M{"mediaType.keyword": HandleMediaType(in.MediaType)}},
}
// 历史同步数据可能为同一个业务标签生成多个 ID。片库按标签名合并这些
// ID,避免搜索结果返回的某个旧 ID 在标签详情页查不到同名内容。
tagIds := HandleTagIds(expandMediaTagAliases(in.TagIds))
must = append(must, tagIds...)
short := HandleLibraryShort(in)
// 根据数据结构组装数据
query := elastic.M{
"query": elastic.M{
"bool": elastic.M{
"must": must,
},
},
"sort": short,
"from": from,
"size": size,
}
res, err := mediamod.SearchByCondWithTotal(query)
if err != nil {
return resp, nil
}
hasNext := false
if uint64(len(res.Hits)) > in.PageSize {
res.Hits = res.Hits[:in.PageSize]
hasNext = true
}
esMedias := make([]mediamod.ESMedia, len(res.Hits))
for i := range res.Hits {
esMedias[i] = res.Hits[i].Source
}
videosIDs := make([]primitive.ObjectID, len(esMedias))
for i, v := range esMedias {
videosIDs[i] = v.ID
}
if len(videosIDs) > int(size) {
hasNext = true
videosIDs = videosIDs[:size]
}
log.Info(fmt.Sprintf("search media mIds:%v", videosIDs))
mediaList, _, err := mediadata.GetListFromCache(in.Filter(videosIDs), 0, int64(in.Limit()), in.Options())
if err != nil {
return resp, err
}
mediaList = orderMediaByIDs(mediaList, videosIDs)
resp.HasNext = hasNext
if len(mediaList) <= 0 {
return resp, nil
}
tagIDs := make([]primitive.ObjectID, 0)
for _, item := range mediaList {
tagIDs = append(tagIDs, item.Tags...)
}
tagMap := mediatagser.GetTagsByIDs2Map(tagIDs)
for _, item := range mediaList {
tags, tagDetails := buildDedupTagsAndDetails(item.Tags, tagMap)
data := &mediamod.AppMediaBase{
ID: item.ID,
SortCode: item.SortCode,
Status: item.Status,
Title: item.Title,
Author: item.Anchor,
HorizontalCover: item.HorizontalCover,
VerticalCover: item.VerticalCover,
Tags: tags,
TagDetails: tagDetails,
Summary: item.Summary,
TotalEpisode: item.TotalEpisode,
UpdateStatus: item.UpdateStatus,
MediaType: item.MediaType,
MediaSubType: item.MediaSubType,
Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide,
Permission: item.Permission,
Price: item.Price,
Direction: item.Direction,
FreeTime: item.FreeTime,
CountComment: item.CountComment,
CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse,
CountCollect: item.CountCollect,
CountLike: item.CountLike,
CountView: item.CountView,
Number: item.Number,
CreatedAt: item.CreatedAt,
UpdateTime: item.UpdateTime,
}
resp.List = append(resp.List, data)
}
resp.HasNext = hasNext
common.Go(func() {
if len(videosIDs) > 0 {
//HandleDiffVideo(vidMods, videosIDs)
}
})
return resp, nil
}
// orderMediaByIDs keeps the final response in the exact order returned by Elasticsearch.
// MongoDB's $in query does not preserve the input ID order, and its copy of a sort field
// can briefly differ from Elasticsearch while indexes are being synchronized.
func orderMediaByIDs(mediaList []*mediamod.Media, ids []primitive.ObjectID) []*mediamod.Media {
mediaByID := make(map[primitive.ObjectID]*mediamod.Media, len(mediaList))
for _, item := range mediaList {
if item != nil {
mediaByID[item.ID] = item
}
}
ordered := make([]*mediamod.Media, 0, len(mediaList))
for _, id := range ids {
if item, ok := mediaByID[id]; ok {
ordered = append(ordered, item)
}
}
return ordered
}
func HandleShort(key int) elastic.A {
return elastic.A{{mediamod.AppLibrarySortField(key): elastic.M{"order": "desc"}}}
}
func HandleLibraryShort(in mediamod.AppLibraryReq) elastic.A {
if HandleMediaType(in.MediaType) == mediamod.MediaTypeDrama && in.SortType == 1 {
return elastic.A{
{"sortCode": elastic.M{"order": "desc"}},
{"createdAt": elastic.M{"order": "desc"}},
}
}
return HandleShort(in.SortType)
}
func HandleTagIds(tagIds []string) elastic.A {
var tagIdsValue = elastic.A{}
var allTagIds []primitive.ObjectID
if len(tagIds) > 0 {
for t := range tagIds {
id, err := primitive.ObjectIDFromHex(tagIds[t])
if err != nil || id.IsZero() {
continue
}
allTagIds = append(allTagIds, id)
}
}
allTagIds = deduplicateObjectIDs(allTagIds)
tagIdsValue = append(tagIdsValue,
elastic.M{"terms": elastic.M{"tags.keyword": allTagIds}},
)
return tagIdsValue
}
func expandMediaTagAliases(tagIDs []string) []string {
expanded := make([]primitive.ObjectID, 0, len(tagIDs))
for _, value := range tagIDs {
id, err := primitive.ObjectIDFromHex(value)
if err != nil || id.IsZero() {
continue
}
expanded = append(expanded, id)
tag, tagErr := mediatagdata.GetInfoFromCache(id)
if tagErr != nil || tag.Name == "" {
continue
}
aliases, aliasErr := mediatagdata.GetTagIdsByNameFromCache(tag.Name)
if aliasErr == nil {
expanded = append(expanded, aliases...)
}
}
expanded = deduplicateObjectIDs(expanded)
result := make([]string, len(expanded))
for i, id := range expanded {
result[i] = id.Hex()
}
return result
}
func deduplicateObjectIDs(ids []primitive.ObjectID) []primitive.ObjectID {
seen := make(map[primitive.ObjectID]struct{}, len(ids))
result := make([]primitive.ObjectID, 0, len(ids))
for _, id := range ids {
if id.IsZero() {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
func Search(uid uint64, in mediamod.AppSearchReq) (resp mediamod.AppElasticSearchResponse, err error) {
var mediaType constant.AcgNewsType
switch in.Kind {
case 1:
mediaType = mediamod.MediaTypeVideo
case 2:
mediaType = mediamod.MediaTypeImage
case 3:
mediaType = mediamod.MediaTypeText
case 4:
mediaType = mediamod.MediaTypeDrama
default:
return resp, nil
}
if in.Kind == 4 {
defer normalizeDramaSearchLists(&resp)
}
resp, err = searchByKeyWord(uid, in, mediaType)
if err != nil {
return resp, err
}
// 本项目使用 关键字 搜索tag名
in.TagName = in.KeyWord
var tagIDFilter interface{}
var responseTagID primitive.ObjectID
if in.Kind == 4 {
tagIDs, tagErr := mediatagdata.GetTagIdsByNameFromCache(in.TagName)
if tagErr != nil {
log.Error("通过标签搜索短剧错误", log.Any("TagName", in.TagName), log.E(tagErr))
return resp, tagErr
}
if len(tagIDs) == 0 {
return resp, nil
}
tagIDFilter = bson.M{"$in": tagIDs}
responseTagID = tagIDs[0]
} else {
tag, tagErr := mediatagdata.GetTagByNameFromCache(in.TagName)
if tagErr != nil {
log.Error("通过标签搜索ACG内容错误", log.Any("TagName", in.TagName), log.E(tagErr))
return resp, tagErr
}
if tag.ID.IsZero() {
return resp, nil
}
tagIDFilter = tag.ID
responseTagID = tag.ID
}
filter := bson.M{
"mediaType": mediaType,
"tags": tagIDFilter,
"status": 1,
"isDelete": false,
}
sort := bson.D{{Key: "createdAt", Value: -1}} // 默认最新
switch in.SortType {
case 1:
// 最新
sort = bson.D{{Key: "createdAt", Value: -1}}
case 2:
//最多观看
sort = bson.D{{Key: "countBrowse", Value: -1}}
}
if in.Kind == 4 {
sort = in.Options()
}
mediaList, _, err := mediadata.GetListFromCache(filter, 0, 6, sort)
if err != nil {
log.Error("通过标签搜索ACG内容错误", log.Any("filter", filter), log.E(err))
return
}
if in.Kind == 4 {
resp.TagMediaList = FillMedias(mediaList, uid, true)
} else {
resp.TagMediaList = mapLegacySearchMedias(mediaList, true)
}
resp.TagID = responseTagID.Hex()
return resp, nil
}
func normalizeDramaSearchLists(resp *mediamod.AppElasticSearchResponse) {
if resp.List == nil {
resp.List = []*mediamod.AppMediaBase{}
}
if resp.TagMediaList == nil {
resp.TagMediaList = []*mediamod.AppMediaBase{}
}
}
func searchByKeyWord(uid uint64, in mediamod.AppSearchReq, mediaType string) (resp mediamod.AppElasticSearchResponse, err error) {
esMedias, err := mediamod.DirectSearch(in.KeyWord, mediaType, int64(in.Skip()), int64(in.Limit()+1))
if err != nil {
log.Error("搜索ACG内容错误", log.E(err))
return resp, nil
}
esMediasLen := len(esMedias)
if esMediasLen == 0 {
return resp, err
}
if esMediasLen > int(in.PageSize) {
resp.HasNext = true
esMedias = esMedias[:esMediasLen-1]
}
var mIds []primitive.ObjectID
for _, v := range esMedias {
mIds = append(mIds, v.ID)
}
filter := bson.M{"_id": bson.M{"$in": mIds}, "mediaType": mediaType, "status": 1, "isDelete": false}
sort := in.Options()
keepSearchOrder := in.Kind == 4 && in.SortType != 2
if keepSearchOrder {
sort = bson.D{}
}
mediaList, _, err := mediadata.GetListFromCache(filter, 0, int64(in.PageSize), sort)
if err != nil {
return resp, err
}
if len(mediaList) <= 0 {
return resp, nil
}
if keepSearchOrder {
// MongoDB 的 $in 不保留 ID 顺序,恢复短剧关键词搜索的相关度顺序。
mediaList = orderMediaByIDs(mediaList, mIds)
}
if in.Kind == 4 {
resp.List = FillMedias(mediaList, uid, true)
} else {
resp.List = mapLegacySearchMedias(mediaList, false)
}
return resp, nil
}
func mapLegacySearchMedias(mediaList []*mediamod.Media, tagResult bool) []*mediamod.AppMediaBase {
result := make([]*mediamod.AppMediaBase, 0, len(mediaList))
for _, item := range mediaList {
data := &mediamod.AppMediaBase{
ID: item.ID, SortCode: item.SortCode, Status: item.Status, Title: item.Title,
HorizontalCover: item.HorizontalCover, VerticalCover: item.VerticalCover,
Tags: item.Tags, Summary: item.Summary, TotalEpisode: item.TotalEpisode,
UpdateStatus: item.UpdateStatus, MediaType: item.MediaType, Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide, Permission: item.Permission,
Price: item.Price, Direction: item.Direction, FreeTime: item.FreeTime,
CountComment: item.CountComment, CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse, CountCollect: item.CountCollect,
CountLike: item.CountLike, CountView: item.CountView, Number: item.Number,
CreatedAt: item.CreatedAt, UpdateTime: item.UpdateTime,
}
if tagResult {
data.Author = item.Anchor
data.MediaSubType = item.MediaSubType
}
result = append(result, data)
}
return result
}
func HandleMediaType(key string) string {
var moduleTypeValue string
switch key {
case "video":
moduleTypeValue = mediamod.MediaTypeVideo
case "image":
moduleTypeValue = mediamod.MediaTypeImage
case "text":
moduleTypeValue = mediamod.MediaTypeText
case "drama":
moduleTypeValue = mediamod.MediaTypeDrama
default:
moduleTypeValue = mediamod.MediaTypeVideo
}
return moduleTypeValue
}
func HandleKindType(key string) int {
var kindValue int
switch key {
case "1":
kindValue = mediamod.RiFan
case "2":
kindValue = mediamod.ThreeD
case "3":
kindValue = mediamod.FanAnimation
case "4":
kindValue = mediamod.Offprint
case "5":
kindValue = mediamod.ComicStrip
case "6":
kindValue = mediamod.KoreanCartoon
default:
kindValue = mediamod.RiFan
}
return kindValue
}
type AppGetTopicReq struct {
SID string `json:"sId" form:"sId"` // 专题ID
Kind int `json:"kind" form:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 )
SortType int `form:"sortType" bson:"sortType"` // 排序 1-最新 2-最多观看 3-最多喜欢(收藏)
Type int `json:"type" form:"type"` // 0-默认专题这些页面获取 1-首页获取(猜你喜欢)
commod.Page
}
func (p *AppGetTopicReq) Filter() bson.M {
var query = bson.M{}
id, _ := primitive.ObjectIDFromHex(p.SID)
if p.Type != 1 && p.SID != "" {
query["sId"] = id
}
if p.Kind > 0 {
query["kind"] = p.Kind
}
if p.Type == 1 {
// 获取专题的模块id
sId, _ := primitive.ObjectIDFromHex(p.SID)
section, _ := modulesectionmod.GetBySectionByID(sId)
query["mId"] = section.SubModuleID
}
query["status"] = 1
query["isDelete"] = false
return query
}
func (p *AppGetTopicReq) Options() bson.D {
var sort bson.D
switch p.SortType {
case 1:
// 1、最新
sort = bson.D{{"createdAt", -1}}
case 2:
// 2、最多观看
sort = bson.D{{"countBrowse", -1}}
case 3:
// 3 最多喜欢(收藏)
sort = bson.D{{"countCollect", -1}}
case 4: // 4 热评排序
sort = bson.D{{"hot", -1}, {"createdAt", -1}}
default:
sort = bson.D{{"createdAt", -1}}
}
return sort
}
type AppGetTopicRes struct {
HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页
Section AppSection `json:"section" bson:"section"` // 播单详情
List []*mediamod.AppMediaBase `json:"list" bson:"list"` // 列表
}
type AppSection struct {
ID primitive.ObjectID `json:"id" bson:"id"` // ID
Name string `json:"name" bson:"name" ` // 播单名称
Cover string `json:"cover" bson:"cover"` // 播单封面
Count int64 `json:"count" bson:"count"` // 作品数量
Type int `json:"type" bson:"type"` // 播单类型
LikeCount int64 `json:"likeCount" bson:"likeCount"` // 点赞数量
CommentCount int64 `json:"commentCount" bson:"commentCount"` // 评论数量
UserID uint64 `json:"userId" bson:"userId"` // 用户ID
UserName string `json:"userName" bson:"userName"` // 用户名称
Portrait string `json:"portrait" bson:"portrait"` // 用户头像
//MediaStatus mediamod.MediaStatus `json:"mediaStatus" bson:"mediaStatus"` // 媒体状态
}
func (p *AppGetTopicReq) getTopicListHomePage() (res AppGetTopicRes, err error) {
mediaList, hasNext, err := mediadata.GetListFromCache(p.Filter(), int64(p.Skip()), int64(p.Limit()), p.Options())
if err != nil {
return res, err
}
res.List = FillNotStatusMedias(mediaList, false)
res.HasNext = hasNext
return
}
// GetTopicList 获取详情
func (p *AppGetTopicReq) GetTopicList(uid uint64) (res AppGetTopicRes, err error) {
if p.Type == 1 {
return p.getTopicListHomePage()
}
// 获取播单数据
sId, _ := primitive.ObjectIDFromHex(p.SID)
section, err := modulesectionmod.GetBySectionByID(sId)
if err != nil {
return res, err
}
res, err = p.getMediaList(uid, section)
common.Go(func() {
t := time.Now()
date := t.Add(-12 * time.Hour).Local().Format("2006-01-02")
sectionstatmod.UpsertSectionHits(sId, date)
})
return
}
// 获取专题相关的acg
func (p *AppGetTopicReq) getMediaList(uid uint64, section modulesectionmod.Section) (res AppGetTopicRes, err error) {
var (
mediaContentIds []primitive.ObjectID
mStatus map[primitive.ObjectID]mediamod.MediaStatus
)
mediaContentIds = append(mediaContentIds, section.ID)
mediaList, hasNext, err := mediadata.GetListFromCache(p.Filter(), int64(p.Skip()), int64(p.Limit()), p.Options())
if err != nil {
return res, err
}
res.HasNext = hasNext
//if len(mediaList) <= 0 {
// var wg sync.WaitGroup
// wg.Add(1)
// common.Go(func() {
// defer wg.Done()
// mStatus = GetUserStatus(uid, mediaContentIds)
// })
// wg.Wait()
// res.Section.MediaStatus = mStatus[section.ID]
// return res, nil
//}
for _, m := range mediaList {
mediaContentIds = append(mediaContentIds, m.ID)
}
//var wg sync.WaitGroup
//wg.Add(1)
//common.Go(func() {
// defer wg.Done()
// mStatus = GetUserStatus(uid, mediaContentIds)
//})
//wg.Wait()
//res.Section.MediaStatus = mStatus[section.ID]
for _, item := range mediaList {
data := &mediamod.AppMediaBase{
ID: item.ID,
SortCode: item.SortCode,
Status: item.Status,
Title: item.Title,
Author: item.Anchor,
HorizontalCover: item.HorizontalCover,
VerticalCover: item.VerticalCover,
UpdateStatus: item.UpdateStatus,
Tags: item.Tags,
Summary: item.Summary,
TotalEpisode: item.TotalEpisode,
MediaType: item.MediaType,
MediaSubType: item.MediaSubType,
Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide,
Permission: item.Permission,
Price: item.Price,
Direction: item.Direction,
FreeTime: item.FreeTime,
CountComment: item.CountComment,
CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse,
CountCollect: item.CountCollect,
CountLike: item.CountLike,
CountView: item.CountView,
Number: item.Number,
MediaStatus: mStatus[item.ID],
CreatedAt: item.CreatedAt,
UpdateTime: item.UpdateTime,
}
res.List = append(res.List, data)
}
return res, nil
}
type AppRecommendReq struct {
TagId string `json:"tagId" form:"tagId"` // 标签id
MediaType string `json:"mediaType" form:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说
IsAudio bool `json:"isAudio" form:"isAudio"` // 是否是音频小说
commod.Page
}
func (p *AppRecommendReq) Filter(media *mediamod.Media) bson.M {
var query = bson.M{
"mediaType": media.MediaType,
"isDelete": false,
}
return query
}
func (p *AppRecommendReq) Options() bson.D {
sort := bson.D{{"_id", -1}, {"createdAt", -1}}
return sort
}
type AppRecommendRes struct {
HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页
List []*MediaRecommend `json:"list" bson:"list"` // 推荐动漫/小说详情页列表
}
type ComicsRecommend struct {
SimilarRecommend []*mediamod.AppMediaBase `json:"similarRecommend"` // 相似推荐
HotRecommend []*MediaRecommend `json:"hotRecommend"` // 热门推荐
DiamondFeatured []*MediaRecommend `json:"diamondFeatured"` // 钻石精选
}
type MediaRecommend struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id
Title string `json:"title" bson:"title"` // 标题
HorizontalCover string `json:"horizontalCover" bson:"horizontalCover"` // 横版封面
VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面
Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 标签
TagDetails []mediamod.TagDetail `json:"tagDetails" bson:"tagDetails"` // 标签对象
TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数
UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 1-连载中 2-已完结
MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说
MediaSubType mediamod.MediaSubType `json:"mediaSubType" bson:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说
Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费
Price int64 `json:"price" bson:"price"` // 价格(整部购买的类型 小说 cos 写真)
Direction int64 `json:"direction" bson:"direction"` // 排版方向 0横 1竖(冗余字段)
CurrentEpisode int `json:"currentEpisode" bson:"currentEpisode"` // 当前更新的集数
Anchor string `json:"anchor" bson:"anchor"` // 作者
}
// GetList 获取推荐列表
func (p *AppRecommendReq) GetList(uid uint64) (res AppRecommendRes, err error) {
tagId, _ := primitive.ObjectIDFromHex(p.TagId)
// 获取动漫/小说推荐
filter := bson.M{"mediaType": p.MediaType}
if !tagId.IsZero() {
filter["tags"] = tagId
}
if p.IsAudio {
filter["mediaSubType"] = 1
}
list, hasNext, err := mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}})
if err != nil {
return
}
res.HasNext = hasNext
for _, v := range list {
res.List = append(res.List, p.MediaToMediaRecommend(v))
}
return res, nil
}
func (p *AppRecommendReq) MediaToMediaRecommend(media *mediamod.Media) *MediaRecommend {
return &MediaRecommend{
ID: media.ID,
Title: media.Title,
HorizontalCover: media.HorizontalCover,
VerticalCover: media.VerticalCover,
Tags: media.Tags,
TagDetails: media.TagDetails,
TotalEpisode: media.TotalEpisode,
UpdateStatus: media.UpdateStatus,
MediaType: media.MediaType,
MediaSubType: media.MediaSubType,
Permission: media.Permission,
Price: media.Price,
CurrentEpisode: media.CurrentEpisode,
Direction: media.Direction,
Anchor: media.Anchor,
}
}
type RankingReq struct {
Type int `json:"type" form:"type"` // 1-日榜 2-周榜 3-月榜 4-总榜 5-年榜
MediaType string `json:"mediaType" form:"mediaType"` // video 动画 image 漫画 text 小说
commod.Page
}
type RankingResp struct {
List []*mediamod.AppMediaBase `json:"list"`
HasNext bool `json:"hasNext"`
}
const (
DailyRank = 1 // 日榜
WeekRank = 2 // 周榜
MonthRank = 3 // 周榜
AllRank = 4 // 总榜
YearRank = 5 // 年榜 180天
)
func (q *RankingReq) List() (res RankingResp, err error) {
start := time.Now()
defer func() {
t := time.Now().UnixMilli() - start.UnixMilli()
log.Info("RankList", log.Any("ranking.Typ", q.Type), log.Any("耗时", fmt.Sprintf("%v ms", t)))
}()
switch q.Type {
case DailyRank, WeekRank, MonthRank:
// 日榜
res, err = q.getRank(q.Type, q.MediaType)
case AllRank, YearRank:
res, err = q.getAllRank(q.MediaType, q.Type == YearRank)
}
return
}
func (q *RankingReq) getAllRank(mediaType string, isYear bool) (res RankingResp, err error) {
// 通过id获取media
filter := bson.M{"mediaType": mediaType, "status": 1, "isDelete": false}
if isYear {
filter["createdAt"] = bson.M{"$gte": timeutil.BeginningOfDay(time.Now())}
}
mediaList, hasNext, err := mediadata.GetListFromCache(filter, int64(q.Skip()), int64(q.Limit()), bson.D{{Key: "countBrowse", Value: -1}})
if err != nil {
return
}
res.List = FillNotStatusMedias(mediaList, false)
res.HasNext = hasNext
return res, nil
}
func (q *RankingReq) getRank(rankinType int, mediaType string) (res RankingResp, err error) {
var list []string
var hasNext bool
switch rankinType {
case DailyRank:
list, _, hasNext = dailytop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit()))
case WeekRank:
list, _, hasNext = weektop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit()))
case MonthRank:
list, _, hasNext = monthtop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit()))
default:
return
}
ids := []primitive.ObjectID{}
for _, v := range list {
id, _ := primitive.ObjectIDFromHex(v)
if id.IsZero() {
continue
}
ids = append(ids, id)
}
// 通过id获取media
mediaList, _, err := mediadata.GetListFromCache(bson.M{"_id": bson.M{"$in": ids}}, 0, int64(q.Limit()), bson.D{})
if err != nil {
return
}
mediaMap := make(map[primitive.ObjectID]*mediamod.Media)
for _, v := range mediaList {
mediaMap[v.ID] = v
}
for _, id := range ids {
item, ok := mediaMap[id]
if !ok {
topasist.Remove(topser.TypeMedia(mediaType), id.Hex())
continue
}
if item.Status != 1 {
topasist.Remove(topser.TypeMedia(mediaType), id.Hex())
}
res.List = append(res.List, FillNotStatusMedias([]*mediamod.Media{item}, false)...)
}
res.HasNext = hasNext
return res, nil
}
type HotReq struct {
Type int `json:"type" form:"type"` // 0-最新热播 1-本月最热 2-上月最热
MediaType string `json:"mediaType" form:"mediaType"` // video 动画(默认不填显示这个) image 漫画 text 小说
commod.Page
}
type HotResp struct {
List []*mediamod.AppMediaBase `json:"list"`
HasNext bool `json:"hasNext"`
}
func (p *HotReq) List() (resp HotResp, err error) {
filter := bson.M{
"status": 1,
"isDelete": false,
"mediaType": "video", // 默认展示动漫
}
now := time.Now()
year, month, day := now.Date()
// 获取今日最后一刻的时间,这里不能直接使用now去做筛选条件,不然根据这些筛选条件生成的redis key,会一直变,导致一直没有命中缓存
now = time.Date(year, month, day, 23, 59, 59, 0, now.Location())
// 默认最新热播
start := now.AddDate(0, 0, -7)
end := now
switch p.Type {
case 1:
// 本月最热
start = now.AddDate(0, -1, 0)
case 2:
// 上月最热
start = now.AddDate(0, -2, 0)
end = now.AddDate(0, -1, 0)
default:
// 判断是否是本周三,再往前多查7天
if carbon.Now().DayOfWeek() <= 3 {
start = now.AddDate(0, 0, -14)
}
}
if p.MediaType != "" {
filter["mediaType"] = p.MediaType
}
filter["createdAt"] = bson.M{"$gte": start, "$lt": end}
mediaList, hasNext, err := mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "countCollect", Value: -1}})
if err != nil {
return
}
for _, item := range mediaList {
tmp := &mediamod.AppMediaBase{
ID: item.ID,
SortCode: item.SortCode,
Status: item.Status,
Title: item.Title,
Author: item.Anchor,
HorizontalCover: item.HorizontalCover,
VerticalCover: item.VerticalCover,
Tags: item.Tags,
Summary: item.Summary,
TotalEpisode: item.TotalEpisode,
MediaType: item.MediaType,
MediaSubType: item.MediaSubType,
Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide,
Permission: item.Permission,
Price: item.Price,
Direction: item.Direction,
FreeTime: item.FreeTime,
CountComment: item.CountComment,
CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse,
CountCollect: item.CountCollect,
CountLike: item.CountLike,
CountView: item.CountView,
Number: item.Number,
CreatedAt: item.CreatedAt,
UpdateTime: item.UpdateTime,
}
resp.List = append(resp.List, tmp)
}
resp.HasNext = hasNext
return
}
type MyBuyReq struct {
MediaType string `json:"mediaType" form:"mediaType"` // video 动画 image 漫画 text 小说 drama 短剧
commod.Page
}
type MyBuyResp struct {
List []*mediamod.AppMediaBase `json:"list"`
HasNext bool `json:"hasNext"`
Total int64 `json:"total"`
}
func (p *MyBuyReq) List(uid uint64) (resp MyBuyResp, err error) {
if p.MediaType == mediamod.MediaTypeDrama {
buyList, total, hasNext, queryErr := media_buy_record_mod.GetMyBuyMediaIDs(uid, p.MediaType, p.Skip(), p.Limit())
if queryErr != nil {
return resp, queryErr
}
ids := make([]primitive.ObjectID, 0, len(buyList))
for _, item := range buyList {
ids = append(ids, item.MediaID)
}
_, mediaMap, queryErr := mediamod.GetListByIds(ids)
if queryErr != nil {
return resp, queryErr
}
ordered := make([]*mediamod.Media, 0, len(ids))
for _, id := range ids {
media, ok := mediaMap[id]
if !ok || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete {
continue
}
copy := media
ordered = append(ordered, &copy)
}
resp.List = FillMedias(ordered, uid, true)
if resp.List == nil {
resp.List = make([]*mediamod.AppMediaBase, 0)
}
resp.Total, resp.HasNext = total, hasNext
return resp, nil
}
res, hasNext, err := media_buy_record_mod.GetMyBuy(uid, p.MediaType, p.Skip(), p.Limit())
if err != nil {
return
}
ids := []primitive.ObjectID{}
for _, v := range res {
ids = append(ids, v.MediaId)
}
mediaList, _, err := mediamod.GetListByIds(ids)
if err != nil {
return
}
for _, item := range mediaList {
tmp := &mediamod.AppMediaBase{
ID: item.ID,
SortCode: item.SortCode,
Status: item.Status,
Title: item.Title,
Author: item.Anchor,
HorizontalCover: item.HorizontalCover,
VerticalCover: item.VerticalCover,
Tags: item.Tags,
Summary: item.Summary,
TotalEpisode: item.TotalEpisode,
MediaType: item.MediaType,
MediaSubType: item.MediaSubType,
Kind: item.Kind,
PermissionIconHide: item.PermissionIconHide,
Permission: item.Permission,
Price: item.Price,
Direction: item.Direction,
FreeTime: item.FreeTime,
CountComment: item.CountComment,
CountPurchases: item.CountPurchases,
CountBrowse: item.CountBrowse,
CountCollect: item.CountCollect,
CountLike: item.CountLike,
CountView: item.CountView,
Number: item.Number,
CreatedAt: item.CreatedAt,
UpdateTime: item.UpdateTime,
}
resp.List = append(resp.List, tmp)
}
resp.HasNext = hasNext
return
}
type (
AppGetMediaListByTagReq struct {
commod.Page
TagId string `form:"tagId" json:"tagId"`
SortType int `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热 3-最多收藏
}
AppGetMediaListByTagRep = AppListRes
)
type TagMediaListReq struct {
TagID string `form:"tagID" json:"tagID" binding:"required"` // 标签id
SortType int `form:"sortType" json:"sortType" binding:"required"` //排序类型 1最多播放,2、最新,3、 最多收藏 "
commod.Page
}
type TagMediaListResp struct {
HasNext bool `json:"hasNext"`
List []*mediamod.AppMediaBase `json:"list"`
}
func (p *TagMediaListReq) GetList() (resp TagMediaListResp, err error) {
tagId, _ := primitive.ObjectIDFromHex(p.TagID)
if tagId.IsZero() {
return
}
filter := bson.M{
"tags": tagId,
"isDelete": false,
"status": 1,
}
sort := bson.D{{"createdAt", -1}}
switch p.SortType {
case 1:
sort = bson.D{{"countBrowse", -1}, {"createdAt", -1}}
case 2:
sort = bson.D{{"createdAt", -1}}
case 3:
sort = bson.D{{"countCollect", -1}, {"createdAt", -1}}
case 4:
sort = bson.D{{"hot", -1}, {"createdAt", -1}}
}
// 获取列表
var data []*mediamod.Media
data, resp.HasNext, err = mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
if err != nil {
return
}
resp.List = FillNotStatusMedias(data, false)
return resp, nil
}