@@ -0,0 +1,349 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package paymentguideser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/paymentguidemod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestSegmentForUser(t *testing.T) {
|
||||
now := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
user *usermod.User
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new unpaid",
|
||||
user: &usermod.User{
|
||||
CreatedAt: now.Add(-time.Hour),
|
||||
WatchCount: 3,
|
||||
},
|
||||
want: paymentguidemod.SegmentNewNeverPaid,
|
||||
},
|
||||
{
|
||||
name: "new user with exhausted free views stays new unpaid",
|
||||
user: &usermod.User{
|
||||
CreatedAt: now.Add(-time.Hour),
|
||||
WatchCount: 0,
|
||||
},
|
||||
want: paymentguidemod.SegmentNewNeverPaid,
|
||||
},
|
||||
{
|
||||
name: "expired is old unpaid",
|
||||
user: &usermod.User{
|
||||
CreatedAt: now.AddDate(0, 0, -30),
|
||||
VipLevel: 2,
|
||||
VipExpireDate: now.Add(-time.Hour),
|
||||
},
|
||||
want: paymentguidemod.SegmentOldNeverPaid,
|
||||
},
|
||||
{
|
||||
name: "active paid upgrade",
|
||||
user: &usermod.User{
|
||||
CreatedAt: now.AddDate(0, 0, -30),
|
||||
VipLevel: 2,
|
||||
VipExpireDate: now.AddDate(0, 1, 0),
|
||||
},
|
||||
want: paymentguidemod.SegmentPaidUpgrade,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SegmentForUser(tt.user); got != tt.want {
|
||||
t.Fatalf("SegmentForUser() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentVersion(t *testing.T) {
|
||||
videos := []vidmod.PaymentGuideVideo{{ID: "video-a"}, {ID: "video-b"}}
|
||||
got := contentVersion(videos)
|
||||
if got == "" {
|
||||
t.Fatal("contentVersion must not be empty")
|
||||
}
|
||||
if got != contentVersion(videos) {
|
||||
t.Fatal("contentVersion must be stable for the same videos")
|
||||
}
|
||||
reordered := []vidmod.PaymentGuideVideo{{ID: "video-b"}, {ID: "video-a"}}
|
||||
if got == contentVersion(reordered) {
|
||||
t.Fatal("contentVersion must change when video order changes")
|
||||
}
|
||||
expanded := append(append([]vidmod.PaymentGuideVideo{}, videos...), vidmod.PaymentGuideVideo{ID: "video-c"})
|
||||
if got == contentVersion(expanded) {
|
||||
t.Fatal("contentVersion must change when configured video count changes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentGuideSwitchesDefaultLegacyHomeToEnabled(t *testing.T) {
|
||||
legacy := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{
|
||||
sysconfmod.VCodePaymentGuideEnabled: true,
|
||||
})
|
||||
if !legacy.global || !legacy.home {
|
||||
t.Fatalf("legacy switches must keep the home scenes enabled: %+v", legacy)
|
||||
}
|
||||
disabled := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{
|
||||
sysconfmod.VCodePaymentGuideEnabled: true,
|
||||
sysconfmod.VCodePaymentGuideHomeEnabled: false,
|
||||
})
|
||||
if !disabled.global || disabled.home {
|
||||
t.Fatalf("configured home switch must be honored: %+v", disabled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuideRespUsesVideosWithoutVideoIDs(t *testing.T) {
|
||||
body, err := json.Marshal(GuideResp{
|
||||
Show: true,
|
||||
Videos: []vidmod.PaymentGuideVideo{{ID: "video-a"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jsonBody := string(body)
|
||||
if !strings.Contains(jsonBody, `"videos"`) {
|
||||
t.Fatalf("response must contain videos: %s", jsonBody)
|
||||
}
|
||||
if strings.Contains(jsonBody, `"videoIds"`) {
|
||||
t.Fatalf("response must not contain videoIds: %s", jsonBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPingGuide(t *testing.T) {
|
||||
freeTrialID := primitive.NewObjectID()
|
||||
homeID := primitive.NewObjectID()
|
||||
contentID := primitive.NewObjectID()
|
||||
configs := map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneHomeNewUserFreeTrial: {
|
||||
ID: freeTrialID,
|
||||
Scene: paymentguidemod.SceneHomeNewUserFreeTrial,
|
||||
Style: "BOTTOM_SHEET",
|
||||
Title: "free trial",
|
||||
Action: paymentguidemod.Action{Type: "NONE"},
|
||||
},
|
||||
paymentguidemod.SceneHomeOldUser: {
|
||||
ID: homeID,
|
||||
Scene: paymentguidemod.SceneHomeOldUser,
|
||||
Style: "BOTTOM_SHEET",
|
||||
Title: "open vip",
|
||||
ProductID: "product-id",
|
||||
DurationSeconds: 60,
|
||||
Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"},
|
||||
},
|
||||
paymentguidemod.SceneVIPContentUpdate: {
|
||||
ID: contentID,
|
||||
Scene: paymentguidemod.SceneVIPContentUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
resp := buildPingGuide(paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentOldNeverPaid, 3, true, configs, nil)
|
||||
if !resp.Enabled || resp.Segment != paymentguidemod.SegmentOldNeverPaid {
|
||||
t.Fatalf("unexpected response header: %+v", resp)
|
||||
}
|
||||
if len(resp.Scenes) != len(pingGuideScenes) {
|
||||
t.Fatalf("scene count = %d, want %d", len(resp.Scenes), len(pingGuideScenes))
|
||||
}
|
||||
freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial]
|
||||
if !freeTrial.Enabled || freeTrial.TotalWatchCount == nil || *freeTrial.TotalWatchCount != 3 || freeTrial.ConfigID != freeTrialID.Hex() {
|
||||
t.Fatalf("unexpected new-user free-trial scene: %+v", freeTrial)
|
||||
}
|
||||
home := resp.Scenes[paymentguidemod.SceneHomeOldUser]
|
||||
if !home.Enabled || home.Show == nil || !*home.Show || home.ConfigID != homeID.Hex() {
|
||||
t.Fatalf("unexpected ordinary scene: %+v", home)
|
||||
}
|
||||
if home.Action == nil || home.Action.Value != "product-id" {
|
||||
t.Fatalf("ordinary scene action missing: %+v", home)
|
||||
}
|
||||
content := resp.Scenes[paymentguidemod.SceneVIPContentUpdate]
|
||||
if !content.Enabled || content.Show != nil || content.ConfigID != "" {
|
||||
t.Fatalf("content-update Ping must expose enabled only: %+v", content)
|
||||
}
|
||||
missing := resp.Scenes[paymentguidemod.SceneVideoBack]
|
||||
if missing.Enabled || missing.Show == nil || *missing.Show {
|
||||
t.Fatalf("missing ordinary scene must be disabled: %+v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPingGuideSuppressesShownAndGlobalDisabled(t *testing.T) {
|
||||
configID := primitive.NewObjectID()
|
||||
configs := map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneVIPCenter: {
|
||||
ID: configID,
|
||||
Scene: paymentguidemod.SceneVIPCenter,
|
||||
Style: "CENTER_POPUP",
|
||||
Title: "shown",
|
||||
Description: "description",
|
||||
Cover: "cover.png",
|
||||
ProductID: "product-id",
|
||||
DurationSeconds: 0,
|
||||
Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"},
|
||||
},
|
||||
}
|
||||
resp := buildPingGuide(
|
||||
paymentGuideSwitchState{global: true, home: true},
|
||||
paymentguidemod.SegmentNormal,
|
||||
3,
|
||||
true,
|
||||
configs,
|
||||
map[primitive.ObjectID]bool{configID: true},
|
||||
)
|
||||
vipCenter := resp.Scenes[paymentguidemod.SceneVIPCenter]
|
||||
if !vipCenter.Enabled || vipCenter.Show == nil || *vipCenter.Show || vipCenter.ConfigID != configID.Hex() {
|
||||
t.Fatalf("shown configuration must retain its display payload: %+v", vipCenter)
|
||||
}
|
||||
if vipCenter.Description == nil || *vipCenter.Description != "description" ||
|
||||
vipCenter.Cover == nil || *vipCenter.Cover != "cover.png" ||
|
||||
vipCenter.ProductID == nil || *vipCenter.ProductID != "product-id" ||
|
||||
vipCenter.DurationSeconds == nil || *vipCenter.DurationSeconds != 0 ||
|
||||
vipCenter.Action == nil || vipCenter.Action.Value != "product-id" {
|
||||
t.Fatalf("shown configuration payload is incomplete: %+v", vipCenter)
|
||||
}
|
||||
|
||||
disabled := buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentNormal, 3, true, configs, nil)
|
||||
if disabled.Enabled {
|
||||
t.Fatal("global-disabled response must be disabled")
|
||||
}
|
||||
for scene, item := range disabled.Scenes {
|
||||
if item.Enabled {
|
||||
t.Fatalf("scene %s must be disabled", scene)
|
||||
}
|
||||
if scene != paymentguidemod.SceneVIPContentUpdate && (item.Show == nil || *item.Show) {
|
||||
t.Fatalf("ordinary scene %s must explicitly return show=false", scene)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPingGuideUsesSharedHomeSwitch(t *testing.T) {
|
||||
freeTrialID := primitive.NewObjectID()
|
||||
homeOldID := primitive.NewObjectID()
|
||||
videoBackID := primitive.NewObjectID()
|
||||
configs := map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneHomeNewUserFreeTrial: {ID: freeTrialID, Scene: paymentguidemod.SceneHomeNewUserFreeTrial},
|
||||
paymentguidemod.SceneHomeOldUser: {ID: homeOldID, Scene: paymentguidemod.SceneHomeOldUser},
|
||||
paymentguidemod.SceneVideoBack: {ID: videoBackID, Scene: paymentguidemod.SceneVideoBack},
|
||||
}
|
||||
resp := buildPingGuide(
|
||||
paymentGuideSwitchState{global: true, home: false},
|
||||
paymentguidemod.SegmentNormal,
|
||||
3,
|
||||
true,
|
||||
configs,
|
||||
nil,
|
||||
)
|
||||
for _, scene := range []string{paymentguidemod.SceneHomeNewUserFreeTrial, paymentguidemod.SceneHomeOldUser} {
|
||||
item := resp.Scenes[scene]
|
||||
if item.Enabled || item.Show == nil || *item.Show || item.ConfigID != "" {
|
||||
t.Fatalf("home scene %s must be disabled by the shared switch: %+v", scene, item)
|
||||
}
|
||||
}
|
||||
videoBack := resp.Scenes[paymentguidemod.SceneVideoBack]
|
||||
if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() {
|
||||
t.Fatalf("non-home scene must not be affected by the home switch: %+v", videoBack)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPingGuideCountdownIsNotSuppressedByImpression(t *testing.T) {
|
||||
configID := primitive.NewObjectID()
|
||||
configs := map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneDiscountCountdown: {
|
||||
ID: configID,
|
||||
Scene: paymentguidemod.SceneDiscountCountdown,
|
||||
Style: "CENTER_POPUP",
|
||||
Title: "限时优惠",
|
||||
Description: "前端本地倒计时",
|
||||
Cover: "countdown.png",
|
||||
ProductID: "product-id",
|
||||
DurationSeconds: 0,
|
||||
Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"},
|
||||
},
|
||||
}
|
||||
resp := buildPingGuide(
|
||||
paymentGuideSwitchState{global: true, home: true},
|
||||
paymentguidemod.SegmentNormal,
|
||||
3,
|
||||
true,
|
||||
configs,
|
||||
map[primitive.ObjectID]bool{configID: true},
|
||||
)
|
||||
countdown := resp.Scenes[paymentguidemod.SceneDiscountCountdown]
|
||||
if !countdown.Enabled || countdown.Show == nil || !*countdown.Show || countdown.ConfigID != configID.Hex() {
|
||||
t.Fatalf("countdown must remain displayable on every App launch: %+v", countdown)
|
||||
}
|
||||
if countdown.DurationSeconds == nil || *countdown.DurationSeconds != 0 {
|
||||
t.Fatalf("countdown must return the complete configured card: %+v", countdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentGuidePingJSONOmitsContentUpdateShow(t *testing.T) {
|
||||
body, err := json.Marshal(buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentUnregistered, 3, false, nil, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded struct {
|
||||
Scenes map[string]map[string]interface{} `json:"scenes"`
|
||||
}
|
||||
var envelope struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Segment string `json:"segment"`
|
||||
Scenes map[string]map[string]interface{} `json:"scenes"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &envelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded.Scenes = envelope.Scenes
|
||||
if _, exists := decoded.Scenes[paymentguidemod.SceneHomeNewUser]; exists {
|
||||
t.Fatalf("legacy HOME_NEW_USER must not be returned in Ping: %s", body)
|
||||
}
|
||||
freeTrial := decoded.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial]
|
||||
if count, exists := freeTrial["totalWatchCount"]; !exists || count != float64(3) {
|
||||
t.Fatalf("new-user free-trial Ping must include totalWatchCount=3: %s", body)
|
||||
}
|
||||
if _, exists := decoded.Scenes[paymentguidemod.SceneVIPContentUpdate]["show"]; exists {
|
||||
t.Fatalf("VIP_CONTENT_UPDATE Ping must omit show: %s", body)
|
||||
}
|
||||
if show, exists := decoded.Scenes[paymentguidemod.SceneHomeOldUser]["show"]; !exists || show != false {
|
||||
t.Fatalf("home Ping scene must include show=false: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentGuidePingJSONKeepsConfiguredFieldsWhenShowFalse(t *testing.T) {
|
||||
configID := primitive.NewObjectID()
|
||||
body, err := json.Marshal(buildPingGuide(
|
||||
paymentGuideSwitchState{global: true, home: true},
|
||||
paymentguidemod.SegmentNormal,
|
||||
3,
|
||||
true,
|
||||
map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneVideoBack: {
|
||||
ID: configID,
|
||||
Scene: paymentguidemod.SceneVideoBack,
|
||||
Style: "CENTER_POPUP",
|
||||
Title: "返回引导",
|
||||
Description: "",
|
||||
Cover: "",
|
||||
ProductID: "",
|
||||
DurationSeconds: 0,
|
||||
Action: paymentguidemod.Action{Type: "NONE"},
|
||||
},
|
||||
},
|
||||
map[primitive.ObjectID]bool{configID: true},
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var envelope struct {
|
||||
Scenes map[string]map[string]interface{} `json:"scenes"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &envelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item := envelope.Scenes[paymentguidemod.SceneVideoBack]
|
||||
for _, field := range []string{"configId", "style", "title", "description", "cover", "productId", "durationSeconds", "action"} {
|
||||
if _, exists := item[field]; !exists {
|
||||
t.Fatalf("configured show=false scene must include %s: %s", field, body)
|
||||
}
|
||||
}
|
||||
if show, ok := item["show"].(bool); !ok || show {
|
||||
t.Fatalf("configured scene must preserve show=false: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPingGuideSuppressesExhaustedNewUserFreeTrialOnly(t *testing.T) {
|
||||
freeTrialID := primitive.NewObjectID()
|
||||
videoBackID := primitive.NewObjectID()
|
||||
resp := buildPingGuide(
|
||||
paymentGuideSwitchState{global: true, home: true},
|
||||
paymentguidemod.SegmentNewNeverPaid,
|
||||
3,
|
||||
false,
|
||||
map[string]paymentguidemod.PaymentGuide{
|
||||
paymentguidemod.SceneHomeNewUserFreeTrial: {
|
||||
ID: freeTrialID,
|
||||
Scene: paymentguidemod.SceneHomeNewUserFreeTrial,
|
||||
Style: "BOTTOM_SHEET",
|
||||
Title: "free trial",
|
||||
Action: paymentguidemod.Action{Type: "NONE"},
|
||||
},
|
||||
paymentguidemod.SceneVideoBack: {
|
||||
ID: videoBackID,
|
||||
Scene: paymentguidemod.SceneVideoBack,
|
||||
Style: "BOTTOM_SHEET",
|
||||
Title: "video back",
|
||||
Action: paymentguidemod.Action{Type: "NONE"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
if resp.Segment != paymentguidemod.SegmentNewNeverPaid {
|
||||
t.Fatalf("segment = %q, want %q", resp.Segment, paymentguidemod.SegmentNewNeverPaid)
|
||||
}
|
||||
freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial]
|
||||
if !freeTrial.Enabled || freeTrial.Show == nil || *freeTrial.Show || freeTrial.ConfigID != freeTrialID.Hex() {
|
||||
t.Fatalf("exhausted free-trial scene must retain config with show=false: %+v", freeTrial)
|
||||
}
|
||||
videoBack := resp.Scenes[paymentguidemod.SceneVideoBack]
|
||||
if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() {
|
||||
t.Fatalf("free-trial exhaustion must not suppress other matching scenes: %+v", videoBack)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user