Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
package vidser
import (
"reflect"
"testing"
"91porn-server/models/commod"
"go.mongodb.org/mongo-driver/bson"
)
func TestACGMediaSortLatest(t *testing.T) {
want := bson.D{
{Key: "latestPublishedAt", Value: -1},
{Key: "contentUpdateTime", Value: -1},
{Key: "createdAt", Value: -1},
{Key: "_id", Value: -1},
}
if got := acgMediaSort(int(commod.New)); !reflect.DeepEqual(got, want) {
t.Fatalf("acgMediaSort(New) = %#v, want %#v", got, want)
}
}
func TestACGMediaSortNonLatestKeepsDefault(t *testing.T) {
want := bson.D{{Key: "createdAt", Value: -1}}
if got := acgMediaSort(int(commod.MostHot)); !reflect.DeepEqual(got, want) {
t.Fatalf("acgMediaSort(MostHot) = %#v, want %#v", got, want)
}
}
+50
View File
@@ -0,0 +1,50 @@
package vidser
import (
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// 元素去重
func RemoveRep(slc []*vidmod.VideoModel) []*vidmod.VideoModel {
if len(slc) < 1024 {
// 切片长度小于1024的时候,循环来过滤
return RemoveRepByLoop(slc)
}
// 大于的时候,通过map来过滤
return RemoveRepByMap(slc)
}
// 通过map主键唯一的特性过滤重复元素
func RemoveRepByMap(slc []*vidmod.VideoModel) []*vidmod.VideoModel {
result := make([]*vidmod.VideoModel, 0, len(slc))
tempMap := map[primitive.ObjectID]struct{}{} // 存放已添加主键
for _, e := range slc {
if _, ok := tempMap[e.ID]; ok { // 主键已添加, 则不重复添加
continue
}
tempMap[e.ID] = struct{}{}
result = append(result, e)
}
return result
}
// 通过两重循环过滤重复元素
func RemoveRepByLoop(slc []*vidmod.VideoModel) []*vidmod.VideoModel {
result := make([]*vidmod.VideoModel, 0, len(slc)) // 存放结果
for i := range slc {
exists := false
for j := range result {
if slc[i].ID == result[j].ID {
exists = true
break
}
}
if exists {
continue
}
result = append(result, slc[i])
}
return result
}
+97
View File
@@ -0,0 +1,97 @@
package vidser
import (
"91porn-server/app/service/vidhelpser"
"91porn-server/common/cachev2"
"91porn-server/common/constant/redisconst"
"91porn-server/common/log"
"91porn-server/models/v/discount_area_mod"
"91porn-server/models/v/discount_area_video_mod"
"91porn-server/models/v/vidmod"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
// GetDiscountArea 获取折扣专区
func GetDiscountArea() (list []discount_area_mod.DiscountArea, err error) {
_, err = cachev2.Classes().CacheTime(redisconst.DiscountAreaExpire).AutoListKey(redisconst.DiscountArea).ResBind(&list).Cache(discount_area_mod.GetAllDiscountArea)
if err != nil {
log.Error(fmt.Sprintf("cachev2 discount_area_mod.GetAllDiscountArea err:%v", err))
return
}
return list, nil
}
// GetDiscountAreaVideos 获取折扣专区下的视频
func GetDiscountAreaVideos(uid uint64, req *DiscountVideoReq) (data *DiscountVideoResp, err error) {
data = &DiscountVideoResp{
List: []*vidmod.VideoInfo{},
}
var videos []*vidmod.VideoModel
key := fmt.Sprintf("DiscountAreaVideoList:%v:%v:%v:%v", req.DiscountId, req.SortType, req.PageSize, req.PageNumber)
_, err = cachev2.Classes().CacheTime(2*time.Minute).AutoListKey(key).ResBind(&videos).Cache(discountAreaVideos, req)
if err != nil {
log.Error(fmt.Sprintf("cachev2 discountAreaVideos err:%v", err))
return
}
if len(videos) == 0 {
log.Warn(fmt.Sprintf("discountAreaVideos is null, discountAreaId:%v", req.DiscountId))
return data, nil
}
if len(videos) > int(req.PageSize) {
data.HasNext = true
videos = videos[:req.PageSize]
}
videoList := vidhelpser.EncodeVideoInfo(uid, videos)
data.List = videoList
return
}
// 获取折扣专区视频列表
func discountAreaVideos(req *DiscountVideoReq) (list []*vidmod.VideoModel, err error) {
// 按照排序类型查询方式不同
switch req.SortType {
case 2:
//按照最热查询
list, err = vidmod.GetVideoListByCond(req.Filter(), req.Options())
default:
list, err = newDiscountAreaVideos(req)
return
}
if err != nil {
log.Error(fmt.Sprintf("discountAreaVideos err:%v", err))
return
}
return
}
func newDiscountAreaVideos(req *DiscountVideoReq) (res []*vidmod.VideoModel, err error) {
// 按照最新添加到折扣专区的顺序排
discountAreaVideoList, _, err := discount_area_video_mod.GetListByCond(req.Filter(), req.Options())
if err != nil {
log.Error(fmt.Sprintf("discount_area_video_mod.GetListByCond err:%v", err))
return
}
vids := []primitive.ObjectID{}
for _, v := range discountAreaVideoList {
vids = append(vids, v.VideoID)
}
// 获取视频
list, err := vidmod.GetVideoListByIDsNoStatus(vids)
if err != nil {
log.Error(fmt.Sprintf("vidmod.GetVideoListByIDs err:%v", err))
return
}
// 按照原来的顺序返回(不要使用sort包排序。按照时间排序,会出现问题)
videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel)
for _, v := range list {
videoMap[v.ID] = v
}
for _, v := range discountAreaVideoList {
video := videoMap[v.VideoID]
res = append(res, video)
}
return
}
+15
View File
@@ -0,0 +1,15 @@
package vidser
import "91porn-server/app/service/vidhelpser"
func applyFreeTrialBadgeToSubModule(uid uint64, data *VideoUnderSubModuleResp) {
if data == nil {
return
}
ctx := vidhelpser.LoadFreeTrialBadgeContext(uid)
vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.AllVideoInfo)
vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.ChosenVideoInfo)
for i := range data.AllSection {
vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.AllSection[i].AllVideoInfo)
}
}
+250
View File
@@ -0,0 +1,250 @@
package vidser
import (
"errors"
"fmt"
"strconv"
"time"
"91porn-server/app/appg"
"91porn-server/common/db"
"91porn-server/common/stderr"
"91porn-server/common/timeutil"
"91porn-server/models/v/useractmod"
"91porn-server/models/v/usermod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
const freeWatchTransactionRetryLimit = 5
type freeWatchConsumeState struct {
isVIP bool
isPublisher bool
freeArea bool
paidVideo bool
viewedToday bool
watchCount uint64
totalCount uint64
}
type freeWatchConsumeDecision struct {
isCan bool
shouldConsume bool
watchCount uint64
}
func decideFreeWatchConsume(state freeWatchConsumeState) freeWatchConsumeDecision {
watchCount := state.watchCount
if watchCount > state.totalCount {
watchCount = state.totalCount
}
if state.isVIP || state.isPublisher || state.freeArea || state.paidVideo || state.viewedToday {
return freeWatchConsumeDecision{isCan: true, watchCount: watchCount}
}
if watchCount == 0 {
return freeWatchConsumeDecision{watchCount: 0}
}
return freeWatchConsumeDecision{
isCan: true,
shouldConsume: true,
watchCount: watchCount,
}
}
// ConsumeFreeWatch 按现有免费观看视频规则消费次数。
// 同一用户、同一视频在同一自然日最多消费一次。
func ConsumeFreeWatch(uid uint64, oid primitive.ObjectID) (vidmod.WatchConsumeResp, error) {
at := time.Now()
day := timeutil.BeginningOfDay(at)
totalCount := TotalFreeWatchCount()
user, err := usermod.FindUserByUIDForNoCache(uid)
if err != nil {
return vidmod.WatchConsumeResp{}, fmt.Errorf("find user: %w", err)
}
if user == nil {
return vidmod.WatchConsumeResp{}, errors.New("find user: empty user")
}
video, err := vidmod.GetVideoInfo(oid.Hex())
if err != nil {
return vidmod.WatchConsumeResp{}, fmt.Errorf("find video: %w", err)
}
if video.ID.IsZero() || video.Status != vidmod.CheckPass {
return vidmod.WatchConsumeResp{}, errors.New("video is missing or unavailable")
}
state := freeWatchConsumeState{
isVIP: !user.VipExpireDate.Before(at),
isPublisher: video.PublisherID == uid,
freeArea: video.FreeArea,
paidVideo: video.Coins > 0,
watchCount: user.WatchCount,
totalCount: totalCount,
}
decision := decideFreeWatchConsume(state)
if decision.isCan && !decision.shouldConsume {
return watchConsumeResponse(decision, totalCount, false), nil
}
viewedToday, err := useractmod.IsViewTodayByNoVipTrans(nil, uid, oid, day)
if err != nil {
return vidmod.WatchConsumeResp{}, fmt.Errorf("find daily watch record: %w", err)
}
state.viewedToday = viewedToday
decision = decideFreeWatchConsume(state)
if !decision.shouldConsume {
return watchConsumeResponse(decision, totalCount, false), nil
}
return consumeFreeWatchTransaction(uid, oid, video, day, at, totalCount)
}
func consumeFreeWatchTransaction(
uid uint64,
oid primitive.ObjectID,
video vidmod.VideoModel,
day time.Time,
at time.Time,
totalCount uint64,
) (vidmod.WatchConsumeResp, error) {
if appg.VideoDB == nil {
return vidmod.WatchConsumeResp{}, errors.New("video database is unavailable")
}
var resp vidmod.WatchConsumeResp
transaction := func(t *db.MongoTool) error {
// The transaction callback may be retried, so reset its result each time.
resp = vidmod.WatchConsumeResp{TotalWatchCount: totalCount}
user, err := usermod.FindUserByUIDTrans(t, uid)
if err != nil {
return fmt.Errorf("find user in transaction: %w", err)
}
if user == nil {
return errors.New("find user in transaction: empty user")
}
viewedToday, err := useractmod.IsViewTodayByNoVipTrans(t, uid, oid, day)
if err != nil {
return fmt.Errorf("find daily watch record in transaction: %w", err)
}
decision := decideFreeWatchConsume(freeWatchConsumeState{
isVIP: !user.VipExpireDate.Before(at),
isPublisher: video.PublisherID == uid,
freeArea: video.FreeArea,
paidVideo: video.Coins > 0,
viewedToday: viewedToday,
watchCount: user.WatchCount,
totalCount: totalCount,
})
if !decision.shouldConsume {
resp = watchConsumeResponse(decision, totalCount, false)
return nil
}
if err := useractmod.UserActInsertTrans(t, useractmod.UserAct{
VID: oid,
PlayWay: useractmod.IsNoVip,
UID: uid,
DailyDate: day,
CreatedAt: at,
ConsumeKey: freeWatchConsumeKey(uid, oid, day),
}); err != nil {
return err
}
remaining := decision.watchCount - 1
updated, err := usermod.UpdateTrans(t, uid, usermod.UserSelector{WatchCount: &remaining})
if err != nil || updated == nil {
return fmt.Errorf("decrement watch count: %w", err)
}
resp = vidmod.WatchConsumeResp{
IsCan: true,
WatchCount: remaining,
TotalWatchCount: totalCount,
Consumed: true,
}
return nil
}
err := runFreeWatchTransactionWithRetry(func() error {
return appg.VideoDB.Trans(transaction)
})
if err == nil {
if resp.Consumed {
// UpdateTrans clears cache inside the transaction; clear once more after
// commit so a concurrent cache refill cannot retain the old count.
usermod.RefreshCache(uid)
}
return resp, nil
}
if !stderr.IsEqual(err, stderr.InsertExistError) {
return vidmod.WatchConsumeResp{}, err
}
// A concurrent request inserted the same daily marker first.
user, findErr := usermod.FindUserByUIDForNoCache(uid)
if findErr != nil {
return vidmod.WatchConsumeResp{}, fmt.Errorf("find user after duplicate consume: %w", findErr)
}
if user == nil {
return vidmod.WatchConsumeResp{}, errors.New("find user after duplicate consume: empty user")
}
decision := freeWatchConsumeDecision{
isCan: true,
watchCount: clampWatchCount(user.WatchCount, totalCount),
}
return watchConsumeResponse(decision, totalCount, false), nil
}
func runFreeWatchTransactionWithRetry(run func() error) error {
var err error
for attempt := 0; attempt < freeWatchTransactionRetryLimit; attempt++ {
err = run()
if err == nil ||
stderr.IsEqual(err, stderr.InsertExistError) ||
!isRetryableFreeWatchTransactionError(err) {
return err
}
// Each run starts a fresh Mongo session. The shared transaction helper
// cannot reliably retry wrapped transient errors in-place.
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
}
return err
}
func isRetryableFreeWatchTransactionError(err error) bool {
if err == nil {
return false
}
var serverErr mongo.ServerError
if !errors.As(err, &serverErr) {
return false
}
return serverErr.HasErrorLabel("TransientTransactionError") ||
serverErr.HasErrorCode(112) || // WriteConflict
serverErr.HasErrorCode(244) || // TransactionAborted
serverErr.HasErrorCode(251) // NoSuchTransaction
}
func watchConsumeResponse(decision freeWatchConsumeDecision, totalCount uint64, consumed bool) vidmod.WatchConsumeResp {
return vidmod.WatchConsumeResp{
IsCan: decision.isCan,
WatchCount: decision.watchCount,
TotalWatchCount: totalCount,
Consumed: consumed,
}
}
func clampWatchCount(watchCount, totalCount uint64) uint64 {
if watchCount > totalCount {
return totalCount
}
return watchCount
}
func freeWatchConsumeKey(uid uint64, oid primitive.ObjectID, day time.Time) string {
return strconv.FormatUint(uid, 10) + ":" + oid.Hex() + ":" + strconv.FormatInt(day.Unix(), 10)
}
@@ -0,0 +1,205 @@
package vidser
import (
"errors"
"fmt"
"testing"
"time"
"91porn-server/common/stderr"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func TestDecideFreeWatchConsume(t *testing.T) {
tests := []struct {
name string
state freeWatchConsumeState
wantCan bool
wantConsume bool
wantRemaining uint64
}{
{
name: "eligible video consumes",
state: freeWatchConsumeState{watchCount: 3, totalCount: 3},
wantCan: true,
wantConsume: true,
wantRemaining: 3,
},
{
name: "already viewed is idempotent",
state: freeWatchConsumeState{viewedToday: true, watchCount: 2, totalCount: 3},
wantCan: true,
wantRemaining: 2,
},
{
name: "vip does not consume",
state: freeWatchConsumeState{isVIP: true, watchCount: 2, totalCount: 3},
wantCan: true,
wantRemaining: 2,
},
{
name: "publisher does not consume",
state: freeWatchConsumeState{isPublisher: true, watchCount: 2, totalCount: 3},
wantCan: true,
wantRemaining: 2,
},
{
name: "free area does not consume",
state: freeWatchConsumeState{freeArea: true, watchCount: 2, totalCount: 3},
wantCan: true,
wantRemaining: 2,
},
{
name: "coin video does not consume",
state: freeWatchConsumeState{paidVideo: true, watchCount: 2, totalCount: 3},
wantCan: true,
wantRemaining: 2,
},
{
name: "no remaining count denies new video",
state: freeWatchConsumeState{watchCount: 0, totalCount: 3},
wantCan: false,
},
{
name: "count is clamped to configured total",
state: freeWatchConsumeState{watchCount: 8, totalCount: 3},
wantCan: true,
wantConsume: true,
wantRemaining: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := decideFreeWatchConsume(tt.state)
if got.isCan != tt.wantCan || got.shouldConsume != tt.wantConsume || got.watchCount != tt.wantRemaining {
t.Fatalf("decision = %+v, want isCan=%v shouldConsume=%v watchCount=%d",
got, tt.wantCan, tt.wantConsume, tt.wantRemaining)
}
})
}
}
func TestFreeWatchConsumeKeyScope(t *testing.T) {
oid := primitive.NewObjectID()
day := time.Date(2026, 7, 28, 0, 0, 0, 0, time.Local)
key := freeWatchConsumeKey(1001, oid, day)
if key != freeWatchConsumeKey(1001, oid, day) {
t.Fatal("same user, video and day must produce the same consume key")
}
if key == freeWatchConsumeKey(1002, oid, day) {
t.Fatal("consume key must be scoped by user")
}
if key == freeWatchConsumeKey(1001, primitive.NewObjectID(), day) {
t.Fatal("consume key must be scoped by video")
}
if key == freeWatchConsumeKey(1001, oid, day.AddDate(0, 0, 1)) {
t.Fatal("consume key must be scoped by day")
}
}
func TestIsRetryableFreeWatchTransactionError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "wrapped transient label",
err: fmt.Errorf("decrement watch count: %w", mongo.CommandError{
Code: 112,
Labels: []string{"TransientTransactionError"},
}),
want: true,
},
{
name: "write conflict code",
err: mongo.CommandError{Code: 112},
want: true,
},
{
name: "write exception transient label",
err: mongo.WriteException{
Labels: []string{"TransientTransactionError"},
},
want: true,
},
{
name: "no such transaction code",
err: fmt.Errorf("retry transaction: %w", mongo.CommandError{Code: 251}),
want: true,
},
{
name: "ordinary database error",
err: errors.New("database unavailable"),
},
{
name: "nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isRetryableFreeWatchTransactionError(tt.err); got != tt.want {
t.Fatalf("isRetryableFreeWatchTransactionError() = %v, want %v", got, tt.want)
}
})
}
}
func TestRunFreeWatchTransactionWithRetry(t *testing.T) {
transient := fmt.Errorf("wrapped write conflict: %w", mongo.CommandError{
Code: 112,
Labels: []string{"TransientTransactionError"},
})
t.Run("eventually succeeds", func(t *testing.T) {
calls := 0
err := runFreeWatchTransactionWithRetry(func() error {
calls++
if calls < 3 {
return transient
}
return nil
})
if err != nil || calls != 3 {
t.Fatalf("err = %v, calls = %d, want nil and 3 calls", err, calls)
}
})
t.Run("stops at retry limit", func(t *testing.T) {
calls := 0
err := runFreeWatchTransactionWithRetry(func() error {
calls++
return transient
})
if err == nil || calls != freeWatchTransactionRetryLimit {
t.Fatalf("err = %v, calls = %d, want error and %d calls",
err, calls, freeWatchTransactionRetryLimit)
}
})
t.Run("duplicate is handled by caller without retry", func(t *testing.T) {
calls := 0
err := runFreeWatchTransactionWithRetry(func() error {
calls++
return stderr.InsertExistError
})
if !stderr.IsEqual(err, stderr.InsertExistError) || calls != 1 {
t.Fatalf("err = %v, calls = %d, want duplicate and 1 call", err, calls)
}
})
t.Run("ordinary error is not retried", func(t *testing.T) {
calls := 0
ordinary := errors.New("database unavailable")
err := runFreeWatchTransactionWithRetry(func() error {
calls++
return ordinary
})
if !errors.Is(err, ordinary) || calls != 1 {
t.Fatalf("err = %v, calls = %d, want ordinary error and 1 call", err, calls)
}
})
}
+409
View File
@@ -0,0 +1,409 @@
package vidser
import (
"91porn-server/app/appg"
"91porn-server/app/service/vidhelpser"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/elastic"
"91porn-server/common/log"
"91porn-server/models/commod"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/mediatagmod"
"91porn-server/models/v/tagmod"
"91porn-server/models/v/vidmod"
"fmt"
"math/rand"
"sync"
"time"
"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"
)
func NewsTypeToSting(in string) string {
var libraryTypeName string
switch in {
case constant.SP:
libraryTypeName = "影片"
case constant.SHORT:
libraryTypeName = "抖音"
case constant.Cartoon:
libraryTypeName = "动漫"
case constant.Comics:
libraryTypeName = "漫画"
case constant.PIC:
libraryTypeName = "图集"
case constant.COVER:
libraryTypeName = "帖子"
}
return libraryTypeName
}
// GetLibrary 获取片库详情
func GetLibrary(uid uint64) (res vidmod.LibraryData, err error) {
var data vidmod.LibraryData
// 获取缓存
str, err := appg.Redis.Get(redisconst.VideoLibraryCache)
if err != nil {
log.Warn(fmt.Sprintf("uid:%v;缓存获取片库信息异常:%v", uid, err))
}
if str != nil {
if err = msgpack.Unmarshal([]byte(*str), &data); err == nil {
return data, nil
}
log.Warn(fmt.Sprintf("uid:%v;解析片库缓存数据异常:%v", uid, err))
}
data.OrderBy = append(data.OrderBy, vidmod.SortKey{
Key: "new",
Name: "最新上架",
}, vidmod.SortKey{
Key: "playNum",
Name: "最多观看",
}, vidmod.SortKey{
Key: "love",
Name: "最多收藏",
})
data.Canvas = append(data.Canvas, vidmod.SortKey{
Key: constant.SP,
Name: NewsTypeToSting(constant.SP),
}, vidmod.SortKey{
Key: constant.SHORT,
Name: NewsTypeToSting(constant.SHORT),
}, vidmod.SortKey{
Key: constant.Cartoon,
Name: NewsTypeToSting(constant.Cartoon),
}, vidmod.SortKey{
Key: constant.Comics,
Name: NewsTypeToSting(constant.Comics),
}, vidmod.SortKey{
Key: constant.PIC,
Name: NewsTypeToSting(constant.PIC),
}, vidmod.SortKey{
Key: constant.COVER,
Name: NewsTypeToSting(constant.COVER),
})
data.PaymentType = append(data.PaymentType, vidmod.SortKey{
Key: "",
Name: "全部",
}, vidmod.SortKey{
Key: "vip",
Name: "VIP",
}, vidmod.SortKey{
Key: "point",
Name: "金币",
})
data.TimeType = append(
data.TimeType, vidmod.SortKey{
Key: "1",
Name: "本月",
}, vidmod.SortKey{
Key: "2",
Name: "三个月内",
}, vidmod.SortKey{
Key: "3",
Name: "半年内",
}, vidmod.SortKey{
Key: "4",
Name: "更久",
})
data.VidTags = append(data.VidTags, vidmod.Tag{
ID: "",
Name: "全部类型",
})
data.ACGTags = append(data.ACGTags, vidmod.Tag{
ID: "",
Name: "全部类型",
})
wg := sync.WaitGroup{}
wg.Add(2)
common.Go(func() {
defer wg.Done()
// 获取最热标签
tags, _, err := tagmod.GetLibraryTagList(commod.Page{PageNumber: 1, PageSize: 20})
if err != nil {
return
}
if len(tags) > 0 {
for _, t := range tags {
data.VidTags = append(data.VidTags, vidmod.Tag{
ID: t.ID.Hex(),
Name: t.TagName,
})
}
}
})
common.Go(func() {
defer wg.Done()
// 获取ACG所有推荐标签
tagFilter := bson.M{"active": true, "isDelete": false, "isDiscovery": true}
tagOp := options.Find().SetLimit(20).SetSort(bson.D{{Key: "sort", Value: -1}})
mediaTags, err := mediatagmod.QueryAllList(tagFilter, tagOp)
if err != nil {
return
}
if len(mediaTags) > 0 {
for _, t := range mediaTags {
data.ACGTags = append(data.ACGTags, vidmod.Tag{
ID: t.ID.Hex(),
Name: t.Name,
})
}
}
})
wg.Wait()
common.Go(func() {
// 加入缓存 随机1-3分钟缓存
random := rand.Intn(120) + 60
d, err := msgpack.Marshal(data)
if err != nil {
return
}
if err = appg.Redis.Set(redisconst.VideoLibraryCache, d, time.Duration(random)*time.Second); err != nil {
log.Warn(fmt.Sprintf("uid:%v, 保存缓存数据异常:%v", uid, err))
}
})
return data, nil
}
type LibraryElasticSearchRequest struct {
Keyword LibraryReq `json:"keyword" bson:"keyword" binding:"required"` // 关键词
UID uint64 `json:"-"` // 用户ID
commod.Page
}
type LibraryElasticSearchResponse struct {
List []vidmod.ESVideo `json:"list"` // list
HasNext bool `json:"hasNext"` // hasNext
Total int `json:"total"` // total
}
type LibraryReq struct {
Canvas SortKey `json:"canvas" bson:"canvas"` // 视频分类
OrderBy SortKey `json:"orderBy" bson:"orderBy"` // 视频排序
Tags SearchTag `json:"tags" bson:"tags"` // 全部标签
PaymentType SortKey `json:"paymentType" bson:"paymentType"` // 付费分类
TimeType SortKey `json:"timeType" bson:"timeType"` // 时间排序
}
type LibraryData struct {
Canvas []SortKey `json:"canvas" bson:"canvas"` // 视频分类
OrderBy []SortKey `json:"orderBy" bson:"orderBy"` // 视频排序
VidTags []SearchTag `json:"vidTags" bson:"vidTags"` // 全部视频标签
ACGTags []SearchTag `json:"acgTags" bson:"acgTags"` // 全部ACG标签
PaymentType []SortKey `json:"paymentType" bson:"paymentType"` // 付费分类
TimeType []SortKey `json:"timeType" bson:"timeType"` // 时间排序
}
type SortKey struct {
Key string `json:"key" bson:"key"` // 健值
Name string `json:"name" bson:"name"` // 健名称
}
type SearchTag struct {
ID string `json:"id" bson:"id"` // 标签ID
Name string `json:"name" bson:"name"` // 标签名称
}
type AppElasticSearchLibraryResponse struct {
List []*vidmod.VideoInfoResp `json:"list" bson:"list"` // 视频列表
AllMediaList []*mediamod.AppMediaBase `json:"allMediaList" bson:"allMediaList"` // ACG列表
HasNext bool `json:"hasNext" bson:"hasNext"` // 是否下一页
Total int `json:"total" bson:"total"` // 总数
}
func (in *LibraryElasticSearchRequest) CheckSearchType() (AppElasticSearchLibraryResponse, error) {
var (
data AppElasticSearchLibraryResponse
err error
)
switch in.Keyword.Canvas.Key {
case constant.SP, constant.SHORT, constant.PIC, constant.COVER:
data, err = in.VidLibrarySearch()
case constant.Cartoon, constant.Comics:
data, err = in.MediaLibrarySearch()
}
if err != nil {
return data, err
}
return data, nil
}
func (in *LibraryElasticSearchRequest) VidLibrarySearch() (AppElasticSearchLibraryResponse, error) {
resp := AppElasticSearchLibraryResponse{}
from := in.Skip64()
size := in.Limit64()
must := elastic.A{
{"term": elastic.M{"newsType.keyword": in.Keyword.Canvas.Key}},
}
if in.Keyword.Tags.ID != "" {
tag := elastic.M{"term": elastic.M{"tags.keyword": in.Keyword.Tags.ID}}
must = append(must, tag)
}
timeType := HandleTimeType(in.Keyword.TimeType.Key)
must = append(must, timeType...)
payType := HandlePayType(in.Keyword.PaymentType.Key)
must = append(must, payType...)
short := HandleShort(in.Keyword.OrderBy.Key)
// 根据数据结构组装数据
query := elastic.M{
"query": elastic.M{
"bool": elastic.M{
"must": must,
},
},
"sort": short,
"from": from,
"size": size + 1,
}
res, err := vidmod.SearchByCondWithTotal(query)
if err != nil {
return resp, nil
}
hasNext := false
videosIDs := make([]primitive.ObjectID, 0)
for _, v := range res.Hits {
videosIDs = append(videosIDs, v.Source.ID)
}
if len(videosIDs) > int(size) {
hasNext = true
videosIDs = videosIDs[:size]
}
vidMods, err := vidmod.GetVideoListByIDs(videosIDs)
if err != nil {
return resp, nil
}
var (
sortVidModels = make([]*vidmod.VideoModel, 0)
vidMap = make(map[primitive.ObjectID]*vidmod.VideoModel)
)
for _, videoModel := range vidMods {
vidMap[videoModel.ID] = videoModel
}
for _, vid := range videosIDs {
if v, ok := vidMap[vid]; ok {
sortVidModels = append(sortVidModels, v)
}
}
data := vidhelpser.NewEncodeVideoInfoNotStatusForSearch(in.UID, sortVidModels)
resp.List = data
resp.HasNext = hasNext
resp.Total = res.Total.Value
// 查询结果数量出现不一致,说名有的帖子不存在了,需要删除
if len(videosIDs) > 0 && len(videosIDs) != len(vidMods) {
common.Go(func() {
HandleDiffVideo(vidMap, videosIDs)
})
}
return resp, nil
}
func HandleDiffVideo(vidMods map[primitive.ObjectID]*vidmod.VideoModel, videosIDs []primitive.ObjectID) {
var source = elastic.M{}
var diffVid []string
for _, vid := range videosIDs {
if _, ok := vidMods[vid]; !ok {
source[vid.Hex()] = vid.Hex()
diffVid = append(diffVid, vid.Hex())
}
}
if len(source) > 0 {
log.Info(fmt.Sprintf("search delete videosIDs:%v", diffVid))
if err := vidmod.DeleteByCond(source); err != nil {
log.Info(fmt.Sprintf("Elastic del data failed, err:%v", err))
return
}
log.Info(fmt.Sprintf("Elastic del data succsss, count:%v", len(source)))
}
}
func HandleTimeType(key string) elastic.A {
var timeTypeValue = elastic.A{}
now := time.Now()
switch key {
case "1": // 本月
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -1, 0)}}},
)
case "2": // 3月内
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -3, 0), "lt": now.AddDate(0, -1, 0)}}},
)
case "3": // 半年内
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -6, 0), "lt": now.AddDate(0, -3, 0)}}},
)
case "4": // 更久
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(-5, 0, 0), "lt": now.AddDate(0, -6, 0)}}},
)
default:
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(-3, -2, 0)}}},
)
}
return timeTypeValue
}
func HandlePayType(key string) elastic.A {
var payTypeValue = elastic.A{}
switch key {
case "free":
payTypeValue = append(payTypeValue,
elastic.M{"term": elastic.M{"status": 3}},
elastic.M{"term": elastic.M{"coins": 0}},
)
case "vip":
payTypeValue = append(payTypeValue,
elastic.M{"term": elastic.M{"status": 1}},
elastic.M{"term": elastic.M{"coins": 0}},
)
case "point":
payTypeValue = append(payTypeValue,
elastic.M{"terms": elastic.M{"status": []int{1, 3}}},
elastic.M{"range": elastic.M{"coins": elastic.M{"gt": 0}}},
)
default:
payTypeValue = append(payTypeValue, elastic.M{"terms": elastic.M{"status": []int{1, 3}}})
}
return payTypeValue
}
func HandleShort(key string) elastic.A {
var payTypeValue elastic.A
switch key {
case "playNum":
payTypeValue = elastic.A{{"playCount": elastic.M{"order": "desc"}}}
case "love":
payTypeValue = elastic.A{{"collectCount": elastic.M{"order": "desc"}}}
case "new":
payTypeValue = elastic.A{{"reviewAt": elastic.M{"order": "desc"}}}
default:
payTypeValue = elastic.A{{"fakeLikeCount": elastic.M{"order": "desc"}}, {"reviewAt": elastic.M{"order": "desc"}}}
}
return payTypeValue
}
+26
View File
@@ -0,0 +1,26 @@
package vidser
import "91porn-server/models/v/vidmod"
// 本文件实现 vidser 视频响应体的 vidmod.M3u8Signable,供 m3u8ticket 零反射签票。
// 目前仅 /api/app/vid/module/:subModuleID (ModuleVideoList) 需要,故只保留其响应体。
// SignM3u8 亚模块下视频:直挂视频、精选视频,以及各专题(AllSection)内的视频都签票。
func (r VideoUnderSubModuleResp) SignM3u8(s vidmod.M3u8Signer) {
vidmod.SignM3u8Resps(s, r.AllVideoInfo)
vidmod.SignM3u8Resps(s, r.ChosenVideoInfo)
for i := range r.AllSection {
r.AllSection[i].SignM3u8(s)
}
}
// SignM3u8 专题下视频列表(由 VideoUnderSubModuleResp 内嵌调用)。
func (r Section) SignM3u8(s vidmod.M3u8Signer) {
vidmod.SignM3u8Resps(s, r.AllVideoInfo)
}
// 编译期断言。
var (
_ vidmod.M3u8Signable = VideoUnderSubModuleResp{}
_ vidmod.M3u8Signable = Section{}
)
+29
View File
@@ -0,0 +1,29 @@
package vidser
import (
"testing"
"91porn-server/models/v/vidmod"
)
type countingSigner struct{ n int }
func (c *countingSigner) SignM3u8URL(field *string, playable, preview bool) { c.n++ }
// 验证亚模块响应体不仅签直挂/精选视频,还会下钻到各专题(AllSection)内的视频——
// 这正是旧反射兜底会覆盖、改接口后最易漏掉的嵌套层级。
func TestVideoUnderSubModuleRespSignsNestedSections(t *testing.T) {
c := &countingSigner{}
resp := VideoUnderSubModuleResp{
AllVideoInfo: []*vidmod.VideoInfoResp{{}},
ChosenVideoInfo: []*vidmod.VideoInfoResp{{}},
AllSection: []Section{
{AllVideoInfo: []*vidmod.VideoInfoResp{{}, {}}},
},
}
resp.SignM3u8(c)
// (1 直挂 + 1 精选)*2 字段 + (2 专题内)*2 字段 = 8
if c.n != 8 {
t.Fatalf("VideoUnderSubModuleResp should offer 8 url fields incl nested sections, got %d", c.n)
}
}
+154
View File
@@ -0,0 +1,154 @@
package vidser
import (
"91porn-server/app/service/mediaser"
"91porn-server/common/constant"
"91porn-server/common/elastic"
"91porn-server/common/log"
"91porn-server/models/v/mediamod"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"time"
)
func (in *LibraryElasticSearchRequest) MediaLibrarySearch() (AppElasticSearchLibraryResponse, error) {
resp := AppElasticSearchLibraryResponse{}
from := int64((in.PageNumber - 1) * (in.PageSize))
size := int64(in.PageSize + 1)
must := elastic.A{
{"term": elastic.M{"mediaType.keyword": HandleMediaCanvasType(in.Keyword.Canvas.Key)}},
}
if in.Keyword.Tags.ID != "" {
tag := elastic.M{"term": elastic.M{"tags.keyword": in.Keyword.Tags.ID}}
must = append(must, tag)
}
timeType := HandleMediaTimeType(in.Keyword.TimeType.Key)
must = append(must, timeType...)
payType := HandleMediaPayType(in.Keyword.PaymentType.Key)
must = append(must, payType...)
short := HandleMediaShort(in.Keyword.OrderBy.Key)
// 根据数据结构组装数据
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
}
if uint64(len(res.Hits)) > in.PageSize {
res.Hits = res.Hits[:in.PageSize]
resp.HasNext = true
}
mIds := make([]primitive.ObjectID, len(res.Hits))
for i, v := range res.Hits {
if !v.Source.ID.IsZero() {
mIds[i] = v.Source.ID
}
}
// 漫画、动漫、文字小说、有声小说
filter := bson.M{"_id": bson.M{"$in": mIds}, "status": 1}
op := options.Find().SetSort(bson.D{{Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}})
mediaBases, err := mediamod.QueryMediaByCond(filter, op)
if err != nil {
log.Error(fmt.Sprintf("mediaLibrarySearch mediamod.QueryMediaByCond err:%v", err))
return resp, err
}
if len(mediaBases) == 0 {
log.Warn("mediaLibrarySearch no video info")
return resp, nil
}
ret := mediaser.FillMedias(mediaBases, in.UID, true)
resp.AllMediaList = ret
resp.Total = res.Total.Value
return resp, nil
}
func HandleMediaTimeType(key string) elastic.A {
var timeTypeValue = elastic.A{}
now := time.Now()
switch key {
case "1":
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -1, 0)}}},
)
case "2":
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -3, 0), "lt": now.AddDate(0, -1, 0)}}},
)
case "3":
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -6, 0), "lt": now.AddDate(0, -3, 0)}}},
)
case "4":
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(-5, 0, 0), "lt": now.AddDate(0, -6, 0)}}},
)
default:
timeTypeValue = append(timeTypeValue,
elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(-3, -2, 0)}}},
)
}
return timeTypeValue
}
func HandleMediaPayType(key string) elastic.A {
var payTypeValue = elastic.A{}
switch key {
case "free":
payTypeValue = append(payTypeValue,
elastic.M{"term": elastic.M{"permission": 2}},
)
case "vip":
payTypeValue = append(payTypeValue,
elastic.M{"term": elastic.M{"permission": 0}},
)
case "point":
payTypeValue = append(payTypeValue,
elastic.M{"term": elastic.M{"permission": 1}},
)
default:
payTypeValue = append(payTypeValue, elastic.M{"terms": elastic.M{"permission": []int{0, 1, 2}}})
}
return payTypeValue
}
func HandleMediaShort(key string) elastic.A {
var payTypeValue elastic.A
switch key {
case "playNum":
payTypeValue = elastic.A{{"countBrowse": elastic.M{"order": "desc"}}}
case "love":
payTypeValue = elastic.A{{"countCollect": elastic.M{"order": "desc"}}}
case "new":
payTypeValue = elastic.A{{"createdAt": elastic.M{"order": "desc"}}}
default:
payTypeValue = elastic.A{{"countBrowse": elastic.M{"order": "desc"}}, {"createdAt": elastic.M{"order": "desc"}}}
}
return payTypeValue
}
func HandleMediaCanvasType(key string) string {
var moduleTypeValue string
switch key {
case constant.Cartoon, constant.Comics:
moduleTypeValue = key
default:
moduleTypeValue = constant.Text
}
return moduleTypeValue
}
+100
View File
@@ -0,0 +1,100 @@
package vidser
import (
"bytes"
"crypto/sha256"
"errors"
"sort"
"time"
"91porn-server/app/service/vidhelpser"
"91porn-server/models/commod"
"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"
)
var ErrRandomRefreshUnsupported = errors.New("the selected sort does not support random refresh")
func RefreshModuleVideos(uid uint64, subModuleID primitive.ObjectID, req RefreshModuleVideosReq) (RefreshModuleVideosResp, error) {
resp := RefreshModuleVideosResp{
AllVideoInfo: []*vidmod.VideoInfoResp{},
RefreshMode: moduleconfmod.RefreshModeRandomTopN,
RefreshToken: req.RefreshToken,
}
subModule, err := moduleconfmod.GetByID(subModuleID)
if err != nil {
return resp, err
}
if !subModule.IsActiveAt(time.Now()) {
return resp, nil
}
subModule.HaiJiaoStyle.EnsureSortRules()
candidateN := 0
for _, rule := range subModule.HaiJiaoStyle.SortRules {
if rule.Val == commod.SortType(req.ModuleSort) && rule.RefreshMode == moduleconfmod.RefreshModeRandomTopN {
candidateN = rule.RandomCandidateN
break
}
}
if candidateN == 0 {
return resp, ErrRandomRefreshUnsupported
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > candidateN {
pageSize = candidateN
}
filter := bson.M{
"mId": subModuleID.Hex(),
"status": vidmod.CheckPass,
"newsType": vidmod.SP,
"chosen": false,
}
opt := options.Find().
SetSort(bson.D{
{Key: "liaoBaTopSort", Value: -1},
{Key: "likeCount", Value: -1},
{Key: "reviewAt", Value: -1},
{Key: "_id", Value: -1},
}).
SetLimit(int64(candidateN)).
SetProjection(bson.M{"richText": 0})
videos, err := vidmod.GetVideoListByCond(filter, opt)
if err != nil {
return resp, err
}
resp.CandidateCount = len(videos)
deterministicOrderVideos(videos, subModuleID.Hex()+":"+req.RefreshToken)
if len(videos) > pageSize {
videos = videos[:pageSize]
}
resp.HasNext = hasNextRefreshCandidate(resp.CandidateCount, len(videos))
resp.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatusForUser(uid, videos)
if resp.AllVideoInfo == nil {
resp.AllVideoInfo = []*vidmod.VideoInfoResp{}
}
return resp, nil
}
func hasNextRefreshCandidate(candidateCount, returnedCount int) bool {
return candidateCount > returnedCount
}
func deterministicOrderVideos(videos []*vidmod.VideoModel, seed string) {
sort.SliceStable(videos, func(i, j int) bool {
left := sha256.Sum256([]byte(seed + ":" + videos[i].ID.Hex()))
right := sha256.Sum256([]byte(seed + ":" + videos[j].ID.Hex()))
cmp := bytes.Compare(left[:], right[:])
if cmp == 0 {
return videos[i].ID.Hex() < videos[j].ID.Hex()
}
return cmp < 0
})
}
+55
View File
@@ -0,0 +1,55 @@
package vidser
import (
"reflect"
"testing"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestDeterministicOrderVideos(t *testing.T) {
source := []*vidmod.VideoModel{
{ID: primitive.NewObjectID()},
{ID: primitive.NewObjectID()},
{ID: primitive.NewObjectID()},
{ID: primitive.NewObjectID()},
}
first := append([]*vidmod.VideoModel(nil), source...)
retry := append([]*vidmod.VideoModel(nil), source...)
deterministicOrderVideos(first, "module:token-a")
deterministicOrderVideos(retry, "module:token-a")
if !reflect.DeepEqual(videoIDs(first), videoIDs(retry)) {
t.Fatal("same refresh token must return the same order")
}
}
func TestHasNextRefreshCandidate(t *testing.T) {
tests := []struct {
name string
candidateCount int
returnedCount int
want bool
}{
{name: "candidate pool has remaining videos", candidateCount: 30, returnedCount: 20, want: true},
{name: "candidate pool exactly fits response", candidateCount: 20, returnedCount: 20, want: false},
{name: "candidate pool is smaller than page size", candidateCount: 12, returnedCount: 12, want: false},
{name: "empty candidate pool", candidateCount: 0, returnedCount: 0, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasNextRefreshCandidate(tt.candidateCount, tt.returnedCount); got != tt.want {
t.Fatalf("hasNextRefreshCandidate(%d, %d) = %t, want %t", tt.candidateCount, tt.returnedCount, got, tt.want)
}
})
}
}
func videoIDs(videos []*vidmod.VideoModel) []primitive.ObjectID {
ids := make([]primitive.ObjectID, len(videos))
for i := range videos {
ids[i] = videos[i].ID
}
return ids
}
+236
View File
@@ -0,0 +1,236 @@
package vidser
import (
"91porn-server/app/appg"
"91porn-server/app/service/vidhelpser"
"91porn-server/common"
"91porn-server/common/constant/redisconst"
"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/sysconfdata"
"91porn-server/models/cache/viddata"
"91porn-server/models/commod"
"91porn-server/models/v/sysconfmod"
"91porn-server/models/v/vidmod"
"encoding/json"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"time"
)
type RankingReq struct {
Type int `json:"type" form:"type"` // 1-日榜 2-周榜 3-月榜 4-总榜 5-年榜
NewsType string `json:"newsType" form:"newsType"` // SP-长视频 SHORT-短视频 COVER-帖子 PIC-图集 HOT-热点榜单
UID uint64 `json:"-" form:"-"`
commod.Page
}
type RankingResp struct {
List []*vidmod.VideoInfo `json:"list"`
HasNext bool `json:"hasNext"`
}
const (
DailyRank = 1 // 日榜
WeekRank = 2 // 周榜
MonthRank = 3 // 周榜
AllRank = 4 // 总榜
YearRank = 5 // 年榜
)
func (q *RankingReq) List() (res RankingResp, err error) {
defer func() {
vidhelpser.ApplyFreeTrialBadgeToVideoInfos(vidhelpser.LoadFreeTrialBadgeContext(q.UID), res.List)
}()
if q.NewsType == "HOT" {
return q.getHotRank()
}
switch q.Type {
case DailyRank, WeekRank, MonthRank:
// 日榜
res, err = q.getRank(q.Type, q.NewsType)
case AllRank, YearRank:
res, err = q.getAllRank(q.NewsType, q.Type == YearRank)
}
return
}
func (q *RankingReq) getHotRank() (res RankingResp, err error) {
// 获取配置的标签
configure, _ := sysconfdata.GetAllFromCache()
tagStrList := configure.GetStrSlice(sysconfmod.VCodeHotRankingTags)
tagIds := []primitive.ObjectID{}
for _, v := range tagStrList {
tagId, _ := primitive.ObjectIDFromHex(v)
if tagId.IsZero() {
continue
}
tagIds = append(tagIds, tagId)
}
if len(tagIds) == 0 {
return
}
key := redisconst.GetHotRankingListCacheKey(tagIds, q.PageNumber, q.PageSize)
s, err := appg.Redis.Get(key)
if err != nil {
return res, err
}
if s != nil {
err = json.Unmarshal([]byte(*s), &res)
if err == nil {
return res, nil
}
}
defer func() {
b, err := json.Marshal(res)
if err != nil {
log.Error("video getRank json.Marshal(res) error:", log.E(err))
return
}
ran := time.Duration(common.RandInt(20, 60) + 240)
appg.Redis.Set(key, string(b), time.Second*ran)
}()
t := timeutil.BeginningOfDay(time.Now())
filter := bson.M{
"tags": bson.M{"$in": tagIds},
"newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT}},
"status": bson.M{"$in": []int{1, 3}},
"reviewAt": bson.M{"$gt": t.AddDate(0, 0, -30)},
}
sorts := bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}
opts := options.Find().SetLimit(q.Limit64()).SetSkip(q.Skip64()).SetSort(sorts)
vidList, hasNext, err := vidmod.FindList(filter, opts)
if err != nil {
return
}
res.List = vidhelpser.EncodeVideoInfoNoUID(vidList)
res.HasNext = hasNext
return res, nil
}
func (q *RankingReq) getAllRank(newsType string, isYear bool) (res RankingResp, err error) {
key := redisconst.GetRankingListCacheKey(q.Type, newsType, q.PageSize, q.PageNumber)
s, err := appg.Redis.Get(key)
if err != nil {
return res, err
}
if s != nil {
err = json.Unmarshal([]byte(*s), &res)
if err == nil {
return res, nil
}
}
defer func() {
b, err := json.Marshal(res)
if err != nil {
log.Error("video getRank json.Marshal(res) error:", log.E(err))
return
}
ran := time.Duration(common.RandInt(20, 60) + 60)
appg.Redis.Set(key, string(b), time.Second*ran)
}()
t := timeutil.BeginningOfDay(time.Now())
t = t.AddDate(-1, -2, 0)
if isYear {
t = t.AddDate(0, -6, 0)
}
filter := bson.M{
"newsType": newsType,
"status": bson.M{"$in": []int{1, 3}},
"reviewAt": bson.M{"$gt": t},
}
sorts := bson.D{{Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}
vidList, hasNext, err := viddata.GetListFromCache(filter, int64(q.Skip()), int64(q.Limit()), sorts)
if err != nil {
return
}
res.List = vidhelpser.EncodeVideoInfoNoUID(vidList)
res.HasNext = hasNext
return res, nil
}
func (q *RankingReq) getRank(rankinType int, newsType string) (res RankingResp, err error) {
var list []string
var hasNext bool
switch rankinType {
case DailyRank:
list, _, hasNext = dailytop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit()))
case WeekRank:
list, _, hasNext = weektop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit()))
case MonthRank:
list, _, hasNext = monthtop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit()))
default:
return
}
periodRankHasContent := len(list) > 0
if !periodRankHasContent && q.Skip64() > 0 {
periodRankHasContent = q.periodRankHasContent(rankinType, newsType)
}
ids := []primitive.ObjectID{}
for _, v := range list {
id, _ := primitive.ObjectIDFromHex(v)
if id.IsZero() {
continue
}
ids = append(ids, id)
}
// 通过id获取列表
vidList, _, err := vidmod.FindList(bson.M{"_id": bson.M{"$in": ids}}, options.Find())
if err != nil {
return
}
vidMap := make(map[primitive.ObjectID]*vidmod.VideoModel)
var videoList []*vidmod.VideoModel
for _, v := range vidList {
vidMap[v.ID] = v
}
for _, id := range ids {
item, ok := vidMap[id]
if !ok || (item.Status != 1 && item.Status != 3) {
topasist.Remove(topser.TypeVideo(newsType), id.Hex())
continue
}
videoList = append(videoList, item)
}
if len(videoList) == 0 && !periodRankHasContent {
videoList, hasNext, err = vidmod.FindCumulativeRanking(newsType, q.Page)
if err != nil {
return res, err
}
}
res.List = vidhelpser.EncodeVideoInfoNoUID(videoList)
res.HasNext = hasNext
return res, nil
}
func (q *RankingReq) periodRankHasContent(rankingType int, newsType string) bool {
var list []string
switch rankingType {
case DailyRank:
list, _, _ = dailytop.GetTopByPage(topser.TypeVideo(newsType), 0, 1)
case WeekRank:
list, _, _ = weektop.GetTopByPage(topser.TypeVideo(newsType), 0, 1)
case MonthRank:
list, _, _ = monthtop.GetTopByPage(topser.TypeVideo(newsType), 0, 1)
}
return len(list) > 0
}
+48
View File
@@ -0,0 +1,48 @@
package vidser
import (
"encoding/json"
"fmt"
"time"
"91porn-server/app/appg"
"91porn-server/common/log"
"91porn-server/models/v/vidmod"
)
const UserTopWork = "UserTopWork:%d"
type WorkList struct {
Works []*vidmod.VideoModel `json:"works"`
}
// GetUserTopWork 获取用户置顶作品:作品排序前三视频
func GetUserTopWork(uid uint64) ([]*vidmod.VideoModel, error) {
var (
w WorkList
key = fmt.Sprintf(UserTopWork, uid)
)
if err := appg.Redis.GetObj(&w, key); err != nil {
log.Error(fmt.Sprintf("用户[%d] redis获取前三视频异常[%v]", uid, err))
return nil, err
}
// 从数据库获取
if len(w.Works) == 0 {
var err error
w.Works, err = vidmod.GetOriginals(uid)
if err != nil {
log.Error(fmt.Sprintf("用户[%d] mongodb获取前三视频异常[%v]", uid, err))
return nil, err
}
data, err := json.Marshal(&w)
if err != nil {
log.Error(fmt.Sprintf("用户[%d] 序列化前三视频异常[%v]", uid, err))
return nil, err
}
if err = appg.Redis.Set(key, data, time.Hour*1); err != nil {
log.Error(fmt.Sprintf("用户[%d] redis保存前三视频异常[%v]", uid, err))
return nil, err
}
}
return w.Works, nil
}
+317
View File
@@ -0,0 +1,317 @@
package vidser
import (
"91porn-server/common/cachev2"
"91porn-server/common/log"
"91porn-server/models/v/discount_area_mod"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/modulesectionmod"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"91porn-server/models/commod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type VideoUnderSubModule struct {
ID primitive.ObjectID `json:"id"` // 视频ID
PublisherID uint64 `json:"publisherID"` // 上传者ID
Title string `json:"title"` // 视频标题
SourceID string `json:"sourceID"` // 视频在仓库中的资源ID
SourceURL string `json:"sourceURL"` // 视频资源地址Path
Cover string `json:"cover"` // 封面大图
CoverThumb string `json:"coverThumb"` // 封面小图
Tags []primitive.ObjectID `json:"tags"` // 标签
PlayCount int `json:"playCount"` // 总播放量
PlayTime uint `json:"playTime"` // 影片长度
ReviewAt time.Time `json:"reviewAt"` // 审核通过时间
}
type VideoUnderSubModuleReq struct {
ModuleSort int `form:"moduleSort,default=1"` // 默认视频排序 1、最新,2、最热/推荐,3、最多播放,4、十分钟以上视频, 5、精华/精选,6、视频 7-最多收藏 8、解锁次数 9、最新热评
TagId string `json:"tagId" form:"tagId"` // 标签id,社区点击标签刷新数据使用
commod.Page
}
func (receive VideoUnderSubModuleReq) Filter(moduleType int, mId *string, TagIds *[]primitive.ObjectID) primitive.M {
now := time.Now().Add(-time.Hour * 24 * 7)
filter := bson.M{}
if mId != nil {
filter["mId"] = mId
}
if TagIds != nil && len(*TagIds) > 0 {
filter["tags"] = bson.M{"$in": TagIds}
}
switch receive.ModuleSort {
case 2:
// 最热
if moduleType != moduleconfmod.Community {
// 首页本周最热
filter["reviewAt"] = bson.M{"$gte": now}
}
if moduleType == moduleconfmod.Community {
filter["reviewAt"] = bson.M{"$gte": time.Now().Add(-time.Hour * 24 * 21)}
}
case 3:
// 推荐
if moduleType == moduleconfmod.Community {
filter["reviewAt"] = bson.M{"$gte": now}
}
case 4:
// 十分钟以上
filter["newsType"] = vidmod.SP
filter["playTime"] = bson.M{"$gte": 600}
case 5:
// 精选
filter["reviewAt"] = bson.M{"$gte": time.Now().Add(-time.Hour * 24 * 14)}
case 6:
// 视频
filter["newsType"] = vidmod.SP
}
filter["status"] = vidmod.CheckPass
return filter
}
func (receive *VideoUnderSubModuleReq) Options(moduleType int) *options.FindOptions {
sort := bson.D{}
switch receive.ModuleSort {
case 1:
// 最新
if moduleType == moduleconfmod.Community {
sort = bson.D{{"reviewAt", -1}}
}
if moduleType == moduleconfmod.HomePage {
// 改为有数值的置顶
sort = bson.D{{"liaoBaTopSort", -1}, {"reviewAt", -1}}
}
case 2:
// 本周最热
sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}}
case 3:
// 推荐
if moduleType == moduleconfmod.Community {
sort = bson.D{{"liaoBaTopSort", -1}, {"playCount", -1}, {"likeCount", -1}, {"reviewAt", -1}}
} else {
sort = bson.D{{"playCount", -1}, {"reviewAt", -1}}
}
case 4:
// 十分钟以上视频
sort = bson.D{{"reviewAt", -1}}
case 5:
// 精华排序
sort = bson.D{{"isChoosen", -1}, {"chosen", -1}, {"commentCount", -1}, {"reviewAt", -1}}
case 6:
// 视频排序
sort = bson.D{{"reviewAt", -1}}
}
return options.Find().
SetSort(sort).
SetSkip(int64(receive.Skip())).
SetLimit(int64(receive.Limit() + 1)).
SetProjection(bson.M{"richText": 0})
}
// VideoUnderSubModuleMap 专题和其视频的关联关系
type VideoUnderSubModuleMap struct {
SectionID string `json:"sectionID"` // 专题ID
SectionName string `json:"sectionName"` // 专题名称
Sort int `json:"-"` // 排序字段,仅仅为了对专题进行排序,不对前端输出
VideoInfo []*vidmod.VideoInfo `json:"videoInfo"` // 视频详情
OriginalBloggerInfo OriginalBloggerInfo `json:"originalBloggerInfo"` // 原创博主信息
ShowType int `json:"showType"`
}
type VideoUnderSubModuleResp struct {
AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo" bson:"allVideoInfo"` // 专题下所有视频
ChosenVideoInfo []*vidmod.VideoInfoResp `json:"chosenVideoInfo" bson:"chosenVideoInfo"` // 精选视频
AllMediaInfo []*mediamod.AppMediaBase `json:"allMediaInfo" bson:"allMediaInfo"` //所有的acg动漫
AllSection []Section `json:"allSection" bson:"allSection"` // 所有专题
HasNext bool `json:"hasNext"` // 是否还有下一页
}
type RefreshModuleVideosReq struct {
ModuleSort int `form:"moduleSort" binding:"required"`
PageSize int `form:"pageSize" binding:"omitempty,min=1,max=30"`
RefreshToken string `form:"refreshToken" binding:"required,max=128"`
}
type RefreshModuleVideosResp struct {
AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo"`
CandidateCount int `json:"candidateCount"`
HasNext bool `json:"hasNext"`
RefreshMode string `json:"refreshMode"`
RefreshToken string `json:"refreshToken"`
}
type Section struct {
SectionID string `json:"sectionID" bson:"sectionID"` // 专题ID
SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称
SectionTitle string `json:"sectionTitle" bson:"sectionTitle"` // 专题的标题
Hot bool `json:"hot" bson:"hot"` // 是否显示hot标识
SectionCover string `json:"sectionCover" bson:"sectionCover"` // 亚模块封面
Sort int `json:"sort" bson:"sort" binding:"required"` // 排序
ShowType int `json:"showType" bson:"showType,omitempty"` // 展示样式
AllTags []Tag `json:"allTags" bson:"allTags"` // 所有标签
AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo" bson:"allVideoInfo"` // 专题下帖子信息
AllMediaInfo []*mediamod.AppMediaBase `json:"allMediaInfo" bson:"allMediaInfo"` // 专题下媒体信息
}
type Tag struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 标签id
TagName string `json:"tagName" bson:"tagName"` // 标签名字 前端使用name
CoverImg string `json:"coverImg" bson:"coverImg"` // 封面图片
HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签
VideoCount int64 `json:"videoCount" bson:"videoCount"` // 使用此标签的视频数量
}
// OriginalBloggerInfo 原创博主相关信息(原创模块需要)
type OriginalBloggerInfo struct {
UID uint64 `json:"uid"` // 用户ID
Name string `json:"name"` // 用户姓名
Portrait string `json:"portrait"` // 头像地址
OfficialCert bool `json:"officialCert"` // 是否官方认证
Summary string `json:"summary"` // 用户简介
}
type VideoUnderSectionReq struct {
SectionID string `uri:"sectionID" binding:"required"`
SortType string `form:"sortType"`
PlayTimeType int `form:"playTimeType"`
}
type VideoUnderSectionResp struct {
Videos []*vidmod.VideoInfoResp `json:"videos"` // 视频列表
Medias []*mediamod.AppMediaBase `json:"medias"` // 动漫/漫画列表
HasNext bool `json:"hasNext"` // 是否还有下一页
OriginalBloggerInfo OriginalBloggerInfo `json:"originalBloggerInfo"` // 原创博主信息
}
type AllVideosOfModuleReq struct {
SubModuleID string `form:"subModuleID"`
SortType string `form:"sortType"` //hot 热度值排序;watch 最多播放;like 最多点赞(收藏);new 最新视频
PlayTimeType int `form:"playTimeType"` //0 默认全部 1 长视频 2 短视频
commod.Page
}
type AllVideosOfModuleResp struct {
Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表
HasNext bool `json:"hasNext"` // 是否还有下一页
}
type SectionResp struct {
ID primitive.ObjectID `json:"id"`
SectionName string `json:"sectionName"`
}
type VidRecommandReq struct {
ID string `bson:"id" json:"id"`
}
type RecommandResp struct {
Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表
HasNext bool `json:"hasNext"` // 是否还有下一页
}
type DiscountVideoReq struct {
DiscountId string `form:"discountId" json:"discountId" binding:"required"`
SortType int `form:"sortType,default=0"` // 排序类型 0-默认 1-最新 2-最热
commod.Page
}
func (receive DiscountVideoReq) Filter() primitive.M {
filter := bson.M{}
discountAreaId, err := primitive.ObjectIDFromHex(receive.DiscountId)
if err != nil {
return filter
}
filter["discountAreaId"] = discountAreaId
switch receive.SortType {
case 2:
// 查询的是videoInfo
filter["status"] = vidmod.CheckPass
default:
}
return filter
}
func (receive *DiscountVideoReq) Options() *options.FindOptions {
sort := bson.D{}
switch receive.SortType {
case 2:
// 最热
sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}}
default:
// 最新
sort = bson.D{{"createdAt", -1}}
}
return options.Find().SetSkip(int64(receive.Skip())).SetLimit(int64(receive.Limit() + 1)).SetSort(sort)
}
type DiscountVideoResp struct {
// 折扣专区
List []*vidmod.VideoInfo `json:"list"`
HasNext bool `json:"hasNext"`
}
// 获取折扣专区列表
type DiscountAreaResp struct {
List []discount_area_mod.DiscountArea
}
type (
VideoListAllReq struct {
SortType int `form:"sortType" json:"sortType"` // 列表类型 1:最新 2:热门 3:本周最热 4:本月最热 5:上月最热
commod.Page
}
VideoListAllResp struct {
Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表
HasNext bool `json:"hasNext"` // 是否还有下一页
}
)
type CommunityRecommendReq struct {
NewsType string `json:"newsType" form:"newsType" binding:"required,oneof=PIC SEED_LINK"` // PIC:套图站热门推荐 SEED_LINK:黄游热门推荐
SortType int `form:"sortType" json:"sortType"` // 排序:最新:1 最多点赞:2 最多观看:3 最多收藏:7 购买次数:8
commod.Page
}
type SectionListReq struct {
Mid string `json:"mid" form:"mid" binding:"required"` // 模块id
commod.Page
}
type SectionListResp struct {
List []modulesectionmod.Section `json:"list"` // 专题列表
HasNext bool `json:"hasNext"` // 是否还有下一页
}
func (p *SectionListReq) GetList() (resp SectionListResp, err error) {
mid, _ := primitive.ObjectIDFromHex(p.Mid)
if mid.IsZero() {
return
}
active, err := moduleconfmod.CanBrowseModule(mid, time.Now())
if err != nil || !active {
return resp, err
}
_, err = cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("moduleSections").ResBind(&resp).Cache(p.getList, mid)
if err != nil {
log.Error("SectionListReq.GetList fail", log.E(err))
return
}
return resp, nil
}
func (p *SectionListReq) getList(mid primitive.ObjectID) (resp SectionListResp, err error) {
list, hasNext, err := modulesectionmod.GetBySubModuleID(mid, p.Page)
if err != nil {
return
}
resp.List = list
resp.HasNext = hasNext
return
}
File diff suppressed because it is too large Load Diff