package paymentguideser import ( "crypto/sha256" "encoding/hex" "fmt" "strings" "time" "91porn-server/app/proto" "91porn-server/app/service/sys_config" "91porn-server/models/cache/sysconfdata" "91porn-server/models/v/moduleconfmod" "91porn-server/models/v/paymentguidemod" "91porn-server/models/v/sysconfmod" "91porn-server/models/v/usermod" "91porn-server/models/v/vidmod" "github.com/google/uuid" "go.mongodb.org/mongo-driver/bson/primitive" ) type GuideResp struct { Show bool `json:"show"` TotalWatchCount *uint64 `json:"totalWatchCount,omitempty"` ConfigID string `json:"configId,omitempty"` ContentVersion string `json:"contentVersion,omitempty"` Segment string `json:"segment"` Style string `json:"style,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Cover string `json:"cover,omitempty"` Videos []vidmod.PaymentGuideVideo `json:"videos,omitempty"` ProductID string `json:"productId,omitempty"` DurationSeconds int64 `json:"durationSeconds,omitempty"` Action *paymentguidemod.Action `json:"action,omitempty"` } type ImpressionReq struct { ConfigID string `json:"configId" binding:"required"` Scene string `json:"scene" binding:"required"` ContentVersion string `json:"contentVersion"` VideoID string `json:"videoId"` RequestID string `json:"requestId" binding:"required"` } const videoPlayCountMultiplier = 99 var pingGuideScenes = []string{ paymentguidemod.SceneHomeNewUserFreeTrial, paymentguidemod.SceneHomeOldUser, paymentguidemod.SceneVideoPreviewEnd, paymentguidemod.SceneDiscountCountdown, paymentguidemod.SceneVideoBack, paymentguidemod.SceneVIPCenter, paymentguidemod.SceneVIPContentUpdate, } // impressionLimitedPingGuideScenes contains only scenes whose show state is // suppressed after the same configuration has already been displayed. The // discount countdown is intentionally excluded because the App controls its // once-per-launch behavior and starts a fresh local countdown on every launch. var impressionLimitedPingGuideScenes = map[string]struct{}{ paymentguidemod.SceneHomeNewUserFreeTrial: {}, paymentguidemod.SceneHomeOldUser: {}, paymentguidemod.SceneVideoPreviewEnd: {}, paymentguidemod.SceneVideoBack: {}, paymentguidemod.SceneVIPCenter: {}, } type paymentGuideSwitchState struct { global bool home bool } func contentVersion(videos []vidmod.PaymentGuideVideo) string { ids := make([]string, 0, len(videos)) for _, video := range videos { ids = append(ids, video.ID) } sum := sha256.Sum256([]byte(strings.Join(ids, ","))) return hex.EncodeToString(sum[:8]) } func SegmentForUser(user *usermod.User) string { switch user.GetPaymentStatusPopup() { case usermod.UserPaymentStatusPopupNewUnpay: return paymentguidemod.SegmentNewNeverPaid case usermod.UserPaymentStatusPopupUnder7DayUnpay, usermod.UserPaymentStatusPopupUnder7DayUnpayNoCountdown, usermod.UserPaymentStatusPopupOver7DayUnpay: return paymentguidemod.SegmentOldNeverPaid case usermod.UserPaymentStatusPopupOver7DayNeedUpgrade: return paymentguidemod.SegmentPaidUpgrade case usermod.UserPaymentStatusPopupMaxVIPLevel: return paymentguidemod.SegmentMaxVIP case usermod.UserPaymentStatusPopupUnregistered: return paymentguidemod.SegmentUnregistered default: return paymentguidemod.SegmentNormal } } func hasFreeTrialRemaining(user *usermod.User) bool { return user != nil && user.WatchCount > 0 } func paymentGuideSwitches() (paymentGuideSwitchState, error) { values, err := sysconfdata.GetBoolsFromSharedCache( sysconfmod.VCodePaymentGuideEnabled, sysconfmod.VCodePaymentGuideHomeEnabled, ) if err != nil { return paymentGuideSwitchState{}, err } return paymentGuideSwitchesFromValues(values), nil } func paymentGuideSwitchesFromValues(values map[sysconfmod.VCode]bool) paymentGuideSwitchState { homeEnabled := true if configured, exists := values[sysconfmod.VCodePaymentGuideHomeEnabled]; exists { homeEnabled = configured } return paymentGuideSwitchState{ global: values[sysconfmod.VCodePaymentGuideEnabled], home: homeEnabled, } } func (s paymentGuideSwitchState) sceneEnabled(scene string) bool { if scene == paymentguidemod.SceneHomeNewUser { return false } if !s.global { return false } if scene == paymentguidemod.SceneHomeNewUserFreeTrial || scene == paymentguidemod.SceneHomeOldUser { return s.home } return true } // GetPingGuide resolves every payment-guide scene in two bounded database // operations: one active-config aggregation and one impression lookup for the // scenes that are suppressed after display. VIP_CONTENT_UPDATE only exposes // its enabled state because videos and contentVersion must be evaluated in real // time by GetGuide. func GetPingGuide(user *usermod.User) (proto.PaymentGuidePing, error) { segment := SegmentForUser(user) totalWatchCount := sys_config.GetTotalWatchCount() freeTrialRemaining := hasFreeTrialRemaining(user) switches, err := paymentGuideSwitches() if err != nil { return buildPingGuide(paymentGuideSwitchState{}, segment, totalWatchCount, freeTrialRemaining, nil, nil), err } if !switches.global { return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), nil } configs, err := paymentguidemod.FindActiveByScenes(pingGuideScenes, segment, time.Now()) if err != nil { return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), err } configIDs := make([]primitive.ObjectID, 0, len(impressionLimitedPingGuideScenes)) for scene, config := range configs { if _, limited := impressionLimitedPingGuideScenes[scene]; limited && switches.sceneEnabled(scene) { configIDs = append(configIDs, config.ID) } } var uid uint64 if user != nil { uid = user.UID } shown, err := paymentguidemod.FindImpressionConfigIDs(uid, configIDs) if err != nil { // 展示记录不可用时失败关闭,避免已展示过的普通弹窗重复弹出。 return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), err } return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, configs, shown), nil } func buildPingGuide( switches paymentGuideSwitchState, segment string, totalWatchCount uint64, freeTrialRemaining bool, configs map[string]paymentguidemod.PaymentGuide, shown map[primitive.ObjectID]bool, ) proto.PaymentGuidePing { resp := proto.PaymentGuidePing{ Enabled: switches.global, Segment: segment, Scenes: make(map[string]proto.PaymentGuidePingScene, len(pingGuideScenes)), } for _, scene := range pingGuideScenes { item := proto.PaymentGuidePingScene{} if scene == paymentguidemod.SceneHomeNewUserFreeTrial { item.TotalWatchCount = &totalWatchCount } if scene != paymentguidemod.SceneVIPContentUpdate { show := false item.Show = &show } config, exists := configs[scene] if !switches.sceneEnabled(scene) || !exists { resp.Scenes[scene] = item continue } item.Enabled = true if scene == paymentguidemod.SceneVIPContentUpdate { resp.Scenes[scene] = item continue } fillPingSceneConfig(&item, config) show := true if _, limited := impressionLimitedPingGuideScenes[scene]; limited { show = !shown[config.ID] } if scene == paymentguidemod.SceneHomeNewUserFreeTrial && !freeTrialRemaining { show = false } item.Show = &show resp.Scenes[scene] = item } return resp } func fillPingSceneConfig(item *proto.PaymentGuidePingScene, config paymentguidemod.PaymentGuide) { item.ConfigID = config.ID.Hex() item.Style = config.Style item.Title = config.Title description := config.Description cover := config.Cover productID := config.ProductID durationSeconds := config.DurationSeconds action := config.Action item.Description = &description item.Cover = &cover item.ProductID = &productID item.DurationSeconds = &durationSeconds item.Action = &action } func GetGuide(uid uint64, scene string) (GuideResp, error) { if !paymentguidemod.ValidScene(scene) { return GuideResp{}, fmt.Errorf("unsupported scene: %s", scene) } user, err := usermod.FindUserByUID(uid) if err != nil { return GuideResp{}, err } segment := SegmentForUser(user) resp := GuideResp{Segment: segment} if scene == paymentguidemod.SceneHomeNewUserFreeTrial { totalWatchCount := sys_config.GetTotalWatchCount() resp.TotalWatchCount = &totalWatchCount } switches, err := paymentGuideSwitches() if err != nil { return resp, err } if !switches.sceneEnabled(scene) { return resp, nil } if scene == paymentguidemod.SceneHomeNewUserFreeTrial && !hasFreeTrialRemaining(user) { return resp, nil } config, err := paymentguidemod.FindActive(scene, segment, time.Now()) if err != nil || config == nil { return resp, err } if scene == paymentguidemod.SceneVIPContentUpdate { excludedModuleIDs, queryErr := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), false) if queryErr != nil { return GuideResp{}, queryErr } resp.Videos, queryErr = vidmod.LatestVIPContent(config.VIPContentVideoLimit(), excludedModuleIDs) if queryErr != nil { return GuideResp{}, queryErr } if len(resp.Videos) == 0 { return resp, nil } for i := range resp.Videos { resp.Videos[i].PlayCount = (resp.Videos[i].PlayCount + resp.Videos[i].FakePlayCount) * videoPlayCountMultiplier } resp.ContentVersion = contentVersion(resp.Videos) } if scene != paymentguidemod.SceneDiscountCountdown { shown, impressionErr := paymentguidemod.HasImpression(uid, config.ID, scene, resp.ContentVersion) if impressionErr != nil { return resp, impressionErr } if shown { resp.Videos = nil resp.ContentVersion = "" return resp, nil } } resp.Show = true resp.ConfigID = config.ID.Hex() resp.Style = config.Style resp.Title = config.Title resp.Description = config.Description resp.Cover = config.Cover resp.ProductID = config.ProductID resp.DurationSeconds = config.DurationSeconds resp.Action = &config.Action return resp, nil } func RecordImpression(uid uint64, req ImpressionReq) error { configID, err := primitive.ObjectIDFromHex(req.ConfigID) if err != nil { return fmt.Errorf("invalid configId") } if !paymentguidemod.ValidScene(req.Scene) { return fmt.Errorf("unsupported scene: %s", req.Scene) } if _, err = uuid.Parse(req.RequestID); err != nil { return fmt.Errorf("invalid requestId") } req.ContentVersion = strings.TrimSpace(req.ContentVersion) if req.Scene == paymentguidemod.SceneVIPContentUpdate && req.ContentVersion == "" { return fmt.Errorf("contentVersion is required") } config, err := paymentguidemod.GetByID(configID) if err != nil { return err } if config.Scene != req.Scene { return fmt.Errorf("scene does not match config") } user, err := usermod.FindUserByUID(uid) if err != nil { return err } if !config.MatchesSegment(SegmentForUser(user)) { return fmt.Errorf("config does not match user segment") } return paymentguidemod.RecordImpression(paymentguidemod.Impression{ UID: uid, ConfigID: configID, Scene: req.Scene, ContentVersion: req.ContentVersion, VideoID: req.VideoID, RequestID: req.RequestID, }) }