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
+130
View File
@@ -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")
}
}
+238
View File
@@ -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
}
+45
View File
@@ -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)
}
}