@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user