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[:]) }