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

563 lines
18 KiB
Go

package searcher
import (
"sync"
"time"
"91porn-server/app/service/searchaccessser"
"91porn-server/app/service/vidhelpser"
"91porn-server/common"
"91porn-server/common/log"
"91porn-server/models/l/payvidlgmod"
"91porn-server/models/v/cmtmod"
"91porn-server/models/v/freeVidmod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/productmod"
"91porn-server/models/v/tagmod"
"91porn-server/models/v/usermod"
"91porn-server/models/v/videodiscountmod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// A A
type A = bson.A
// M M
type M = bson.M
// D D
type D = bson.D
// E E
type E = bson.E
// ObjectID ObjectID
type ObjectID = primitive.ObjectID
// VideoInfo VideoInfo
type VideoInfo struct {
ID *ObjectID `json:"id" bson:"_id"`
NewsType *string `json:"newsType"` //帖子类型, VID,视频帖子,COVER
Title *string `json:"title,omitempty"` //视频标题
Tags []TagInfo `json:"tags"` //视频标签
SourceID *string `json:"sourceID,omitempty"` //视频在仓库中的资源ID
SourceURL *string `json:"sourceURL,omitempty"` //视频资源地址Path
H265Url *string `json:"h265Url"` // H.265 视频资源地址
PlayTime *uint `json:"playTime,omitempty"` //影片长度
Cover *string `json:"cover,omitempty"` //封面大图
CoverThumb *string `json:"coverThumb,omitempty"` //封⾯小图
SeriesCover []string `json:"seriesCover"` //封面套图
Via *string `json:"via,omitempty"` //来源 自拍,上传
PlayCount *int `json:"playCount,omitempty"` //总的播放量量(假数据)
LikeCount *int `json:"likeCount,omitempty"` //点赞数(假数据)
PayCount *int `json:"payCount,omitempty"` //购买数
CommentCount *int `json:"commentCount,omitempty"` //评论数(假数据)
ShareCount *int `json:"shareCount,omitempty"` //分享数(假数据)
Coins *int64 `json:"coins,omitempty"` //折扣后价格
Size *int `json:"size,omitempty"` //文件大小 byte
Status *int `json:"status,omitempty"` //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0
Resolution *string `json:"resolution,omitempty"` //分辨率
Ratio *float64 `json:"ratio,omitempty"` //宽高比
MimeType *string `json:"mimeType,omitempty"` //视频格式类型
Actor *string `json:"actor,omitempty"`
FreeTime *int `json:"freeTime,omitempty"` //免费观影时长
CreatedAt *time.Time `json:"createdAt,omitempty"` //创建时间
ReviewedAt *time.Time `json:"reviewAt,omitempty"` // 审核时间
UInfo vidmod.Publisher `json:"publisher,omitempty" bson:"publisher"` //用户信息
FreeArea *bool `json:"freeArea,omitempty"` //免费专区
PublisherID *uint64 `json:"-"` //发布者ID(user)
LocationID *primitive.ObjectID `json:"-"` //位置ID
IsTopping *bool `json:"isTopping"`
IsRecommend *bool `json:"isRecommend"` //力荐
IsChoosen *bool `json:"isChoosen"` //置精
OriginCoins *int64 `json:"originCoins"` // 视频原价
SearchAccessToken *string `json:"searchAccessToken,omitempty"`
ShowFreeTrialBadge bool `json:"showFreeTrialBadge" bson:"-"`
FreeTrialRemaining uint64 `json:"freeTrialRemaining" bson:"-"`
CanUseFreeTrial bool `json:"canUseFreeTrial" bson:"-"`
}
// VideoMap VideoMap
func videoMap(vidList []ObjectID, uid uint64, allowSearchOnly bool) (map[ObjectID]*VideoInfo, error) {
if len(vidList) == 0 {
return make(map[ObjectID]*VideoInfo), nil
}
videoMap, vInfo, err := vidmod.SearchVideoMap(vidList)
if err != nil {
return nil, err
}
blockedModules := make(map[string]struct{})
if allowSearchOnly {
excludedModules, excludeErr := moduleconfmod.ExcludedSearchModuleIDs()
if excludeErr != nil {
return nil, excludeErr
}
for _, moduleID := range excludedModules {
blockedModules[moduleID] = struct{}{}
}
} else {
moduleIDs := make([]string, 0, len(vInfo))
for _, video := range vInfo {
if video.MID != "" {
moduleIDs = append(moduleIDs, video.MID)
}
}
blockedModules, err = moduleconfmod.BlockedOutsideSearchModuleIDs(moduleIDs, time.Now())
if err != nil {
return nil, err
}
}
var (
vidInfoMap = make(map[ObjectID]*VideoInfo, len(videoMap))
tidUniqueMap = make(map[ObjectID]byte)
mFree = map[uint64]bool{}
self *usermod.User
videoDiscLog videodiscountmod.VideoDiscountLog
largestDiscount int
wg sync.WaitGroup
mUser map[uint64]*vidmod.Publisher
)
uids := make([]uint64, len(vInfo))
for i, v := range vInfo {
uids[i] = v.PublisherID
}
wg.Add(5)
common.Go(func() {
defer wg.Done()
self, err = usermod.FindUserByUID(uid)
if err != nil {
log.Error("VideoMap: usermod.FindUserByUID", log.Any("uid", uid), log.E(err))
return
}
})
common.Go(func() {
defer wg.Done()
mUser = vidhelpser.GetUserInfo2Map(uid, uids)
})
common.Go(func() {
defer wg.Done()
largestDiscount, err = productmod.GetLargestDiscountVIPCard()
if err != nil {
log.Error("VideoMap GetLargestDiscountVIPCard", log.Any("uid", uid), log.E(err))
}
})
common.Go(func() {
defer wg.Done()
mFree = freeVidmod.GetFreeVideoMapByUID(uid)
})
common.Go(func() {
defer wg.Done()
videoDiscLog, err = videodiscountmod.GetByUID(uid)
if err != nil {
log.Error("VideoMap videodiscountmod.GetByUID", log.Any("uid", uid), log.E(err))
}
})
wg.Wait()
freeTrialContext := vidhelpser.FreeTrialBadgeContextForUser(uid, self)
for vid, vidInfo := range videoMap {
videoPtr := vidInfo
if _, blocked := blockedModules[videoPtr.MID]; blocked {
continue
}
status := videoPtr.Status
if status == 3 {
status = 1
}
var uInfo vidmod.Publisher
if mUser[vidInfo.PublisherID] != nil {
uInfo = *mUser[vidInfo.PublisherID]
} else {
//处理作者被删除的情况
if len(mUser) > 0 {
for i := range mUser {
uInfo = *mUser[i]
break
}
}
}
newsType := vidmod.SP
if len(videoPtr.NewsType) != 0 {
newsType = videoPtr.NewsType
}
discountCoins := vidhelpser.CalcVideoCoins(videoPtr.Coins, videoPtr.PublisherID, self, mFree, videoDiscLog,
largestDiscount, nil)
h265URL := vidhelpser.H265URLForApp(videoPtr)
vInfo := VideoInfo{
ID: &videoPtr.ID,
NewsType: &newsType,
Title: &videoPtr.Title, //视频标题
Tags: []TagInfo{}, //视频标签
SourceID: &videoPtr.SourceID, //视频在仓库中的资源ID
SourceURL: &videoPtr.SourceURL, //视频资源地址Path
H265Url: &h265URL, // H.265 视频资源地址
PlayTime: &videoPtr.PlayTime, //影片长度
Cover: &videoPtr.Cover, //封面大图
CoverThumb: &videoPtr.CoverThumb, //封⾯小图
SeriesCover: videoPtr.SeriesCover,
Via: &videoPtr.Via, //来源 自拍,上传
PlayCount: &videoPtr.FakePlayCount, //总的播放量量(假数据)
LikeCount: &videoPtr.FakeLikeCount, //点赞数(假数据)
CommentCount: &videoPtr.FakeCommentCount, //评论数(假数据)
ShareCount: &videoPtr.FakeShareCount, //分享数(假数据)
Size: &videoPtr.Size, //文件大小 byte
Ratio: &videoPtr.Ratio, //分辨率
Status: &status, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0
Resolution: &videoPtr.Resolution, //分辨率
MimeType: &videoPtr.MimeType, //视频格式类型
Actor: &videoPtr.Actor,
FreeTime: &videoPtr.FreeTime, //免费观影时长
PublisherID: &videoPtr.PublisherID, //发布者ID(user)
FreeArea: &videoPtr.FreeArea, //免费专区
LocationID: &videoPtr.Location, //位置ID
CreatedAt: &videoPtr.CreatedAt, //创建时间
ReviewedAt: &videoPtr.ReviewAt,
UInfo: uInfo,
IsTopping: &videoPtr.IsTopping,
IsChoosen: &videoPtr.IsChoosen,
IsRecommend: &videoPtr.IsRecommend,
OriginCoins: &videoPtr.Coins, // 定价
Coins: &discountCoins, // 折扣价
}
vInfo.ShowFreeTrialBadge, vInfo.FreeTrialRemaining, vInfo.CanUseFreeTrial = freeTrialContext.Fields(
newsType,
videoPtr.Coins,
videoPtr.FreeArea,
videoPtr.PublisherID,
)
if allowSearchOnly {
token, tokenErr := searchaccessser.Issue(uid, videoPtr.ID.Hex(), time.Now())
if tokenErr != nil {
return nil, tokenErr
}
vInfo.SearchAccessToken = &token
}
if len(videoPtr.Tags) != 0 {
for _, tid := range videoPtr.Tags {
tidUniqueMap[tid] = 0
}
}
//fill video Map
vidInfoMap[vid] = &vInfo
}
tidList := make([]ObjectID, len(tidUniqueMap))
i := 0
for k := range tidUniqueMap {
tidList[i] = k
i++
}
//获取所有tagInfo List
tInfoList, err := TagInfoListByTIDList(&uid, tidList)
if err != nil {
return nil, err
}
tInfoMap := ToTagMap(tInfoList)
//vidInfoMap填充tagInfo
for vid, vidInfo := range vidInfoMap {
video := videoMap[vid]
tidList := video.Tags
tInfoList := make([]TagInfo, 0, len(tidList))
for _, t := range video.Tags {
tInfo := tInfoMap[t]
tInfoList = append(tInfoList, tInfo)
}
vidInfo.Tags = tInfoList
}
return vidInfoMap, nil
}
// VidStatus VidStatus
type VidStatus struct {
HasPaid *bool `json:"hasPaid,omitempty"` //已支付
}
// VidAndStatus VidAndStatus
type VidAndStatus struct {
VID ObjectID
*VidStatus
}
// VidStatusMap VidStatusMap
func VidStatusMap(vidList []ObjectID, uid uint64) map[ObjectID]VidStatus {
var (
payUniqList = make([]string, 0, len(vidList))
payUniqMap = make(map[ObjectID]string, len(vidList))
)
for _, v := range vidList {
payUniq := payvidlgmod.Unique(uid, v)
payUniqList = append(payUniqList, payUniq)
payUniqMap[v] = payUniq
}
payStatueMap, err := payvidlgmod.PayStatueMap(payUniqList)
if err != nil {
return map[ObjectID]VidStatus{}
}
vidStatusMap := make(map[ObjectID]VidStatus, len(vidList))
for _, v := range vidList {
payHash := payUniqMap[v]
payStatue := payStatueMap[payHash]
vidStatusMap[v] = VidStatus{
HasPaid: &payStatue,
}
}
return vidStatusMap
}
// Publisher Publisher
type Publisher = UserRes
// VideoRes VideoRes
type VideoRes struct {
VideoInfo `bson:",inline"` //视屏
LocInfo `json:"location" bson:"location"` //位置
VidStatus `json:"vidStatus" bson:"vidStatus"` //用户与视频状态信息
Publisher `json:"publisher" bson:"publisher"` //发布者
}
// GetVideoRes GetVideoRes
func GetVideoRes(vid ObjectID, vInfoMap map[ObjectID]*VideoInfo, publMap map[uint64]*UserRes, locMap map[ObjectID]*LocInfo, statusMap map[ObjectID]VidStatus) VideoRes {
res := VideoRes{}
video := vInfoMap[vid]
if video == nil {
return res
}
res.VideoInfo = *video
if video.PublisherID != nil {
publisher := publMap[*video.PublisherID]
if publisher != nil {
res.Publisher = *publisher
}
}
if video.LocationID != nil {
loc := locMap[*video.LocationID]
if loc != nil {
res.LocInfo = *loc
}
}
status := statusMap[vid]
res.VidStatus = status
return res
}
func getVideoParentCommentCountMap(vidList []ObjectID) map[ObjectID]int {
res := make(map[ObjectID]int, len(vidList))
if len(vidList) == 0 {
return res
}
countMap, err := cmtmod.CmtFindParentCountByObjIDs(cmtmod.CmtTypeVideo, vidList)
if err != nil {
log.Error("GetVideoResList CmtFindParentCountByObjIDs error", log.E(err), log.Any("vidList", vidList))
return res
}
for id, count := range countMap {
res[id] = int(count)
}
return res
}
// GetVideoResList GetVideoResList
func GetVideoResList(uid uint64, vidList []ObjectID) ([]VideoRes, error) {
return getVideoResList(uid, vidList, false)
}
// GetVideoResListForSearch 保留当前仅能通过搜索入口访问的视频。
func GetVideoResListForSearch(uid uint64, vidList []ObjectID) ([]VideoRes, error) {
return getVideoResList(uid, vidList, true)
}
func getVideoResList(uid uint64, vidList []ObjectID, allowSearchOnly bool) ([]VideoRes, error) {
videoCount := len(vidList)
//vid->video Map
videoMap, err := videoMap(vidList, uid, allowSearchOnly)
if err != nil {
return nil, err
}
commentCountMap := getVideoParentCommentCountMap(vidList)
//Publisher
//发布者ID List
pidList := make([]uint64, 0, videoCount)
for _, video := range videoMap {
if video.PublisherID == nil {
continue
}
pidList = append(pidList, *video.PublisherID)
}
//LocationID List
lidList := make([]ObjectID, 0, videoCount)
for _, v := range videoMap {
if v.LocationID == nil {
continue
}
lidList = append(lidList, *v.LocationID)
}
var (
wg = sync.WaitGroup{}
publMap = make(map[uint64]*UserRes)
locMap = make(map[ObjectID]*LocInfo)
statusMap = make(map[ObjectID]VidStatus)
)
//一共3个map,每个携程填充一个Map
wg.Add(3)
common.Go(func() {
//PublisherID->User map
defer wg.Done()
m, err := UserResMap(pidList, uid)
if err != nil {
log.Error("GetVideoResList PublisherID->User error", log.E(err))
return
}
publMap = m
})
//Location map
common.Go(func() {
//LocationID->Location map
defer wg.Done()
m, err := LocMap(lidList)
if err != nil {
log.Error("GetVideoResList Location map error", log.E(err))
return
}
locMap = m
})
//Video Status map
common.Go(func() {
//Vid->视屏状态 map
defer wg.Done()
statusMap = VidStatusMap(vidList, uid)
})
wg.Wait()
//组装 []VidInfo
videoResList := make([]VideoRes, 0, videoCount)
for _, vid := range vidList {
info := GetVideoRes(vid, videoMap, publMap, locMap, statusMap)
if info.Publisher.UID == nil {
log.Warn("Publisher UID is invalid")
continue
}
if info.VideoInfo.ID != nil {
commentCount := commentCountMap[*info.VideoInfo.ID]
info.CommentCount = &commentCount
}
info.Publisher.HasFollowed = &videoMap[vid].UInfo.HasFollowed
isFree := true
info.FreeArea = &isFree
videoResList = append(videoResList, info)
}
return videoResList, nil
}
// RichVidRes RichVidRes
type RichVidRes struct {
VideoInfo `bson:",inline"` //视屏
LocInfo `json:"location" bson:"location"` //位置
VidStatus `json:"vidStatus" bson:"vidStatus"` //用户与视频状态信息
Publisher `json:"publisher,omitempty" bson:"publisher"` //发布者
}
// GetRichVideoRes GetRichVideoRes
func GetRichVideoRes(vid ObjectID, vInfoMap map[ObjectID]*VideoInfo, publMap map[uint64]*UserRes, locMap map[ObjectID]*LocInfo, statusMap map[ObjectID]VidStatus) RichVidRes {
res := GetVideoRes(vid, vInfoMap, publMap, locMap, statusMap)
if res.VideoInfo.ID == nil {
return RichVidRes{}
}
richRes := RichVidRes(res)
return richRes
}
// GetRichVideoResList GetRichVideoResList
func GetRichVideoResList(uid uint64, vidList []ObjectID) ([]RichVidRes, error) {
return getRichVideoResList(uid, vidList, false)
}
// GetRichVideoResListForSearch 保留当前仅能通过搜索入口访问的视频。
func GetRichVideoResListForSearch(uid uint64, vidList []ObjectID) ([]RichVidRes, error) {
return getRichVideoResList(uid, vidList, true)
}
func getRichVideoResList(uid uint64, vidList []ObjectID, allowSearchOnly bool) ([]RichVidRes, error) {
videoCount := len(vidList)
//vid->video Map
videoMap, err := videoMap(vidList, uid, allowSearchOnly)
if err != nil {
return nil, err
}
//Publisher
//发布者ID List
pidList := make([]uint64, 0, videoCount)
for _, video := range videoMap {
if video.PublisherID == nil {
continue
}
pidList = append(pidList, *video.PublisherID)
}
//LocationID List
lidList := make([]ObjectID, 0, videoCount)
for _, v := range videoMap {
if v.LocationID == nil {
continue
}
lidList = append(lidList, *v.LocationID)
}
var (
wg = sync.WaitGroup{}
publMap = make(map[uint64]*UserRes)
locMap = make(map[ObjectID]*LocInfo)
statusMap = make(map[ObjectID]VidStatus)
)
//一共4个map,每个携程填充一个Map
wg.Add(3)
common.Go(func() {
//PublisherID->User map
defer wg.Done()
m, err := UserResMap(pidList, uid)
if err != nil {
log.Error("GetVideoResList PublisherID->User error", log.E(err))
return
}
publMap = m
})
//Location map
common.Go(func() {
//LocationID->Location map
defer wg.Done()
m, err := LocMap(lidList)
if err != nil {
log.Error("GetVideoResList Location map error", log.E(err))
return
}
locMap = m
})
//Video Status map
common.Go(func() {
//Vid->视屏状态 map
defer wg.Done()
statusMap = VidStatusMap(vidList, uid)
})
wg.Wait()
//组装 []RichVidRes
videoResList := make([]RichVidRes, 0, videoCount)
for _, vid := range vidList {
info := GetRichVideoRes(vid, videoMap, publMap, locMap, statusMap)
if info.Publisher.UID == nil {
continue
}
videoResList = append(videoResList, info)
}
return videoResList, nil
}
// VideoListByKeyword 通过关键字匹配title和tagName获取VID List
func VideoListByKeyword(keyword string, skip int64, limit int64) ([]vidmod.ObjectID, error) {
tags, err := tagmod.GetTagsByKeyword(keyword, 0, 3)
if err != nil {
return nil, err
}
tidList := make([]primitive.ObjectID, len(tags))
for i, tag := range tags {
tidList[i] = tag.ID
}
return vidmod.VIDListByKeywordOrTags(keyword, tidList, skip, limit)
}