@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user