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, ©) } 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 }