@@ -0,0 +1,130 @@
|
||||
package shareser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
recommendShareEventIDMaxLength = 128
|
||||
recommendShareEventDedupTTL = 7 * 24 * time.Hour
|
||||
recommendShareDailyDedupTTL = 48 * time.Hour
|
||||
recommendShareCleanupTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type recommendShareDeduper interface {
|
||||
SetNXContext(
|
||||
ctx context.Context,
|
||||
key string,
|
||||
value interface{},
|
||||
expiration time.Duration,
|
||||
) (bool, error)
|
||||
DelContext(ctx context.Context, keys ...string) (int64, error)
|
||||
}
|
||||
|
||||
type recommendShareIncrement func(context.Context, primitive.ObjectID) error
|
||||
|
||||
func incrementRecommendShareOnce(
|
||||
ctx context.Context,
|
||||
deduper recommendShareDeduper,
|
||||
uid uint64,
|
||||
videoID primitive.ObjectID,
|
||||
eventID string,
|
||||
now time.Time,
|
||||
) error {
|
||||
return incrementRecommendShareOnceWith(
|
||||
ctx,
|
||||
deduper,
|
||||
uid,
|
||||
videoID,
|
||||
eventID,
|
||||
now,
|
||||
func(ctx context.Context, id primitive.ObjectID) error {
|
||||
return vidmod.IncrementRecommendInteractionContext(
|
||||
ctx,
|
||||
id,
|
||||
vidmod.RecommendInteractionShare,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func incrementRecommendShareOnceWith(
|
||||
ctx context.Context,
|
||||
deduper recommendShareDeduper,
|
||||
uid uint64,
|
||||
videoID primitive.ObjectID,
|
||||
eventID string,
|
||||
now time.Time,
|
||||
increment recommendShareIncrement,
|
||||
) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if deduper == nil {
|
||||
return fmt.Errorf("recommend share deduper is nil")
|
||||
}
|
||||
if uid == 0 || videoID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
eventID = strings.TrimSpace(eventID)
|
||||
if len(eventID) > recommendShareEventIDMaxLength {
|
||||
return fmt.Errorf("share eventId exceeds %d characters", recommendShareEventIDMaxLength)
|
||||
}
|
||||
|
||||
// 无论客户端是否提供 eventId,同一用户、视频、自然日只累计一次推荐分享分,
|
||||
// 避免客户端通过不断生成新 eventId 刷高权重。eventId 仍用于跨日重试幂等。
|
||||
day := now.In(time.FixedZone("CST", 8*60*60)).Format("20060102")
|
||||
dailyKey := recommendShareDedupKey(uid, videoID, "day:"+day)
|
||||
dailyAcquired, err := deduper.SetNXContext(
|
||||
ctx, dailyKey, "1", recommendShareDailyDedupTTL,
|
||||
)
|
||||
if err != nil || !dailyAcquired {
|
||||
return err
|
||||
}
|
||||
|
||||
keysToRelease := []string{dailyKey}
|
||||
if eventID != "" {
|
||||
eventKey := recommendShareDedupKey(uid, videoID, "event:"+eventID)
|
||||
eventAcquired, eventErr := deduper.SetNXContext(
|
||||
ctx, eventKey, "1", recommendShareEventDedupTTL,
|
||||
)
|
||||
if eventErr != nil || !eventAcquired {
|
||||
releaseRecommendShareKeys(deduper, keysToRelease...)
|
||||
return eventErr
|
||||
}
|
||||
keysToRelease = append(keysToRelease, eventKey)
|
||||
}
|
||||
|
||||
err = increment(ctx, videoID)
|
||||
if err != nil {
|
||||
// 推荐累计失败时释放幂等标记,使同一事件后续重试仍有补偿机会。
|
||||
releaseRecommendShareKeys(deduper, keysToRelease...)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func releaseRecommendShareKeys(deduper recommendShareDeduper, keys ...string) {
|
||||
if deduper == nil || len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
cleanupCtx, cancel := context.WithTimeout(
|
||||
context.Background(), recommendShareCleanupTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
_, _ = deduper.DelContext(cleanupCtx, keys...)
|
||||
}
|
||||
|
||||
func recommendShareDedupKey(uid uint64, videoID primitive.ObjectID, scope string) string {
|
||||
sum := sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", uid, videoID.Hex(), scope)))
|
||||
return "recommend:short:share-dedup:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package shareser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type fakeRecommendShareDeduper struct {
|
||||
mu sync.Mutex
|
||||
keys map[string]bool
|
||||
ttls []time.Duration
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeRecommendShareDeduper) SetNXContext(
|
||||
_ context.Context,
|
||||
key string,
|
||||
_ interface{},
|
||||
expiration time.Duration,
|
||||
) (bool, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.err != nil {
|
||||
return false, f.err
|
||||
}
|
||||
f.ttls = append(f.ttls, expiration)
|
||||
if f.keys == nil {
|
||||
f.keys = make(map[string]bool)
|
||||
}
|
||||
if f.keys[key] {
|
||||
return false, nil
|
||||
}
|
||||
f.keys[key] = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *fakeRecommendShareDeduper) DelContext(
|
||||
_ context.Context,
|
||||
keys ...string,
|
||||
) (int64, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var removed int64
|
||||
for _, key := range keys {
|
||||
if f.keys[key] {
|
||||
delete(f.keys, key)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceDeduplicatesEvent(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
now := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)
|
||||
calls := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, "event-1", now, increment,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("increment calls = %d, want 1", calls)
|
||||
}
|
||||
if len(store.ttls) != 3 ||
|
||||
store.ttls[0] != recommendShareDailyDedupTTL ||
|
||||
store.ttls[1] != recommendShareEventDedupTTL ||
|
||||
store.ttls[2] != recommendShareDailyDedupTTL {
|
||||
t.Fatalf("dedupe TTL calls = %v", store.ttls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceCapsDifferentEventsPerDay(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
now := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)
|
||||
calls := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
for _, eventID := range []string{"event-1", "event-2", "event-3"} {
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, eventID, now, increment,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("increment calls = %d, want one per user/video/day", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceEventRetryDoesNotConsumeNextDayQuota(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
first := time.Date(2026, 7, 31, 15, 59, 0, 0, time.UTC)
|
||||
nextDay := first.Add(2 * time.Minute)
|
||||
calls := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
for _, step := range []struct {
|
||||
eventID string
|
||||
now time.Time
|
||||
}{
|
||||
{eventID: "event-1", now: first},
|
||||
{eventID: "event-1", now: nextDay},
|
||||
{eventID: "event-2", now: nextDay},
|
||||
} {
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, step.eventID, step.now, increment,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("increment calls = %d, want one on each day", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceLegacyScopesByCSTDay(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
incremented := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
incremented++
|
||||
return nil
|
||||
}
|
||||
first := time.Date(2026, 7, 31, 15, 59, 0, 0, time.UTC)
|
||||
second := first.Add(2 * time.Minute)
|
||||
for _, now := range []time.Time{first, first, second} {
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, "", now, increment,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if incremented != 2 {
|
||||
t.Fatalf("increment calls = %d, want 2 days", incremented)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceRedisFailureDoesNotIncrement(t *testing.T) {
|
||||
wantErr := errors.New("redis unavailable")
|
||||
store := &fakeRecommendShareDeduper{err: wantErr}
|
||||
calls := 0
|
||||
err := incrementRecommendShareOnceWith(
|
||||
context.Background(),
|
||||
store,
|
||||
123,
|
||||
primitive.NewObjectID(),
|
||||
"event",
|
||||
time.Now(),
|
||||
func(context.Context, primitive.ObjectID) error {
|
||||
calls++
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("increment calls = %d, want 0", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceReleasesKeyAfterFailure(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
wantErr := errors.New("injected")
|
||||
calls := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return wantErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, "event", time.Now(), increment,
|
||||
); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("first error = %v, want injected", err)
|
||||
}
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, "event", time.Now(), increment,
|
||||
); err != nil {
|
||||
t.Fatalf("retry error = %v", err)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("increment calls = %d, want 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceConcurrent(t *testing.T) {
|
||||
store := &fakeRecommendShareDeduper{}
|
||||
id := primitive.NewObjectID()
|
||||
var mu sync.Mutex
|
||||
calls := 0
|
||||
increment := func(context.Context, primitive.ObjectID) error {
|
||||
mu.Lock()
|
||||
calls++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 32; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := incrementRecommendShareOnceWith(
|
||||
context.Background(), store, 123, id, "event", time.Now(), increment,
|
||||
); err != nil {
|
||||
t.Errorf("increment error = %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if calls != 1 {
|
||||
t.Fatalf("increment calls = %d, want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementRecommendShareOnceRejectsLongEventID(t *testing.T) {
|
||||
err := incrementRecommendShareOnceWith(
|
||||
context.Background(),
|
||||
&fakeRecommendShareDeduper{},
|
||||
123,
|
||||
primitive.NewObjectID(),
|
||||
string(make([]byte, recommendShareEventIDMaxLength+1)),
|
||||
time.Now(),
|
||||
func(context.Context, primitive.ObjectID) error { return nil },
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected long eventId error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package shareser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/versionser"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/sourcemod"
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/e/sharemod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// GeneratorQrCode 生成二维码
|
||||
func GeneratorQrCode(uid uint64, content, videoID string) (code stderr.Code, data interface{}) {
|
||||
return GeneratorQrCodeContext(context.Background(), uid, content, videoID, "")
|
||||
}
|
||||
|
||||
// GeneratorQrCodeContext 生成二维码,并按分享事件幂等累计短视频推荐分。
|
||||
func GeneratorQrCodeContext(
|
||||
ctx context.Context,
|
||||
uid uint64,
|
||||
content, videoID, eventID string,
|
||||
) (code stderr.Code, data interface{}) {
|
||||
return generatorQrCodeContextWithScore(
|
||||
ctx,
|
||||
uid,
|
||||
content,
|
||||
videoID,
|
||||
eventID,
|
||||
func(
|
||||
ctx context.Context,
|
||||
uid uint64,
|
||||
videoID primitive.ObjectID,
|
||||
eventID string,
|
||||
now time.Time,
|
||||
) error {
|
||||
return incrementRecommendShareOnce(
|
||||
ctx, appg.Redis, uid, videoID, eventID, now,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GeneratorDramaQrCodeContext 生成短剧分享二维码并幂等累计分享互动分。
|
||||
func GeneratorDramaQrCodeContext(
|
||||
ctx context.Context,
|
||||
uid uint64,
|
||||
content, mediaID, contentID, eventID string,
|
||||
) (code stderr.Code, data interface{}) {
|
||||
id, err := primitive.ObjectIDFromHex(mediaID)
|
||||
if err != nil {
|
||||
return stderr.ErrParamError, ""
|
||||
}
|
||||
media, err := mediamod.GetInfo(id)
|
||||
if err != nil || media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete {
|
||||
return stderr.ErrParamError, ""
|
||||
}
|
||||
if contentID != "" {
|
||||
cid, parseErr := primitive.ObjectIDFromHex(contentID)
|
||||
if parseErr != nil {
|
||||
return stderr.ErrParamError, ""
|
||||
}
|
||||
episode, episodeErr := mediacontentmod.GetInfo(cid, true)
|
||||
if episodeErr != nil || episode.MediaID != id || episode.MediaType != mediamod.MediaTypeDrama || episode.IsDelete {
|
||||
return stderr.ErrParamError, ""
|
||||
}
|
||||
}
|
||||
return generatorQrCodeContextWithScore(
|
||||
ctx, uid, content, mediaID, eventID,
|
||||
func(ctx context.Context, uid uint64, mediaID primitive.ObjectID, eventID string, now time.Time) error {
|
||||
return incrementRecommendShareOnceWith(
|
||||
ctx, appg.Redis, uid, mediaID, eventID, now,
|
||||
func(_ context.Context, id primitive.ObjectID) error {
|
||||
return mediamod.IncreaseCountShareByID(id, 1)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type recommendShareScoreFunc func(
|
||||
context.Context,
|
||||
uint64,
|
||||
primitive.ObjectID,
|
||||
string,
|
||||
time.Time,
|
||||
) error
|
||||
|
||||
func generatorQrCodeContextWithScore(
|
||||
ctx context.Context,
|
||||
uid uint64,
|
||||
content, videoID, eventID string,
|
||||
score recommendShareScoreFunc,
|
||||
) (code stderr.Code, data interface{}) {
|
||||
tmp := strconv.FormatUint(uid, 10)
|
||||
url := content + "&share=true&shareuid=" + tmp
|
||||
png, err := qrcode.Encode(url, qrcode.Medium, 256)
|
||||
if err != nil {
|
||||
e := fmt.Sprintf("GeneratorQrCode error: uid(%d), content(%s), err(%+v)\n", uid, content, err)
|
||||
log.Error(e)
|
||||
return stderr.ErrNetWorkBusy, ""
|
||||
}
|
||||
if id, parseErr := primitive.ObjectIDFromHex(videoID); parseErr == nil && score != nil {
|
||||
if incErr := score(ctx, uid, id, eventID, time.Now()); incErr != nil {
|
||||
// 二维码已经生成,推荐统计是旁路能力;瞬时 Redis/Mongo
|
||||
// 故障不能破坏旧分享主流程,失败通过日志和监控告警。
|
||||
log.Error("increment recommend share failed", log.E(incErr), log.Any("videoID", videoID))
|
||||
}
|
||||
}
|
||||
return stderr.Success, sharemod.VShareResp{QrCode: png}
|
||||
}
|
||||
|
||||
// GetShareCnt 获取视频分享次数
|
||||
func GetShareCnt(videoID string) (code stderr.Code, data interface{}) {
|
||||
cnt, err := vidmod.GetShareCount(videoID)
|
||||
if err != nil {
|
||||
return stderr.ErrNetWorkBusy, ""
|
||||
}
|
||||
data = sharemod.VShareCntResp{VidelID: videoID, Cnt: cnt}
|
||||
return stderr.Success, data
|
||||
}
|
||||
|
||||
// Info 获取分享的视频信息以及推荐列表
|
||||
func Info(id primitive.ObjectID, sysType string) (*sharemod.List, *stderr.CustomErr) {
|
||||
video, err := vidmod.GetByIDFromRedis(id.Hex())
|
||||
if err != nil {
|
||||
return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查找视频失败")
|
||||
}
|
||||
if video.ID.IsZero() {
|
||||
return nil, stderr.NewCustomErr(stderr.Failure, "视频未找到", "视频未找到")
|
||||
}
|
||||
blockedModules, err := moduleconfmod.BlockedOutsideSearchModuleIDs([]string{video.MID}, time.Now())
|
||||
if err != nil {
|
||||
return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查找视频失败")
|
||||
}
|
||||
if _, blocked := blockedModules[video.MID]; blocked {
|
||||
return nil, stderr.NewCustomErr(stderr.CodeEmptyData, "视频未找到", "视频未找到")
|
||||
}
|
||||
var picUrl, landUrl string
|
||||
_, pl := sourcemod.PingList()
|
||||
if len(pl) > 0 {
|
||||
for _, v := range pl {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if landUrl != "" && picUrl != "" {
|
||||
break
|
||||
}
|
||||
if v.Type == sourcemod.Image && len(v.Domain) > 0 {
|
||||
picUrl = v.Domain[rand.Intn(len(v.Domain))].Url
|
||||
continue
|
||||
}
|
||||
if v.Type == sourcemod.ShareURL && len(v.Domain) > 0 {
|
||||
landUrl = v.Domain[rand.Intn(len(v.Domain))].Url
|
||||
}
|
||||
}
|
||||
}
|
||||
vsis, err := vidmod.GetShareListFromRedis()
|
||||
if err != nil {
|
||||
return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查询分享视频推荐列表失败")
|
||||
}
|
||||
vsis, err = filterShareRecommendations(vsis)
|
||||
if err != nil {
|
||||
return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查询分享视频推荐列表失败")
|
||||
}
|
||||
ver, _, _, _, _, _, err := versionser.AdvVersionAnnounThreeServer("1.0.1", sysType)
|
||||
if err != nil {
|
||||
log.Error("查询版本信息失败", log.E(err))
|
||||
return nil, stderr.NewCustomErr(stderr.Failure, err.Error(), "查询下载链接失败")
|
||||
}
|
||||
if len(ver.DownloadLink) == 0 {
|
||||
return nil, stderr.NewCustomErr(stderr.Failure, "", "无有效的app下载地址")
|
||||
}
|
||||
l := sharemod.List{
|
||||
Title: video.Title,
|
||||
Cover: video.Cover,
|
||||
SourceUrl: video.SourceURL,
|
||||
LandUrl: landUrl,
|
||||
PicUrl: picUrl,
|
||||
Tags: []string{"暗网幼女", "人兽重口", "校园霸凌", "灌醉迷奸", "处女破处", "捉奸现场", "疯狂孕妇", "黑鬼草人妻",
|
||||
"缅北禁区", "全家乱操", "黑料吃瓜", "摄像破解", "群P激战", "AI换脸", "SM调教", "反差母狗"},
|
||||
DownloadUrl: ver.DownloadLink[0],
|
||||
List: vsis,
|
||||
}
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
func filterShareRecommendations(items []vidmod.ShareInfo) ([]vidmod.ShareInfo, error) {
|
||||
if len(items) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
ids := make([]primitive.ObjectID, 0, len(items))
|
||||
for _, item := range items {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
videos, err := vidmod.GetVideosByIDs(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
excludedModules, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
excludedSet := make(map[string]struct{}, len(excludedModules))
|
||||
for _, moduleID := range excludedModules {
|
||||
excludedSet[moduleID] = struct{}{}
|
||||
}
|
||||
excludedVideos := make(map[primitive.ObjectID]struct{})
|
||||
for _, video := range videos {
|
||||
if video == nil {
|
||||
continue
|
||||
}
|
||||
if _, excluded := excludedSet[video.MID]; excluded {
|
||||
excludedVideos[video.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
filtered := make([]vidmod.ShareInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
if _, excluded := excludedVideos[item.ID]; excluded {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package shareser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/e/sharemod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestGeneratorQrCodeKeepsShareSuccessWhenRecommendScoreFails(t *testing.T) {
|
||||
scoreCalls := 0
|
||||
code, data := generatorQrCodeContextWithScore(
|
||||
context.Background(),
|
||||
123,
|
||||
"https://example.test/video?id=1",
|
||||
primitive.NewObjectID().Hex(),
|
||||
"event-1",
|
||||
func(
|
||||
context.Context,
|
||||
uint64,
|
||||
primitive.ObjectID,
|
||||
string,
|
||||
time.Time,
|
||||
) error {
|
||||
scoreCalls++
|
||||
return errors.New("recommend score unavailable")
|
||||
},
|
||||
)
|
||||
|
||||
if code != stderr.Success {
|
||||
t.Fatalf("code=%v, want success", code)
|
||||
}
|
||||
response, ok := data.(sharemod.VShareResp)
|
||||
if !ok || len(response.QrCode) == 0 {
|
||||
t.Fatalf("data=%T %#v, want non-empty QR code", data, data)
|
||||
}
|
||||
if scoreCalls != 1 {
|
||||
t.Fatalf("score calls=%d, want 1", scoreCalls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user