350 lines
11 KiB
Go
350 lines
11 KiB
Go
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,
|
|
})
|
|
}
|