Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+381
View File
@@ -0,0 +1,381 @@
package paymentguidemod
import (
"context"
"fmt"
"strings"
"time"
"91porn-server/common/db"
"91porn-server/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var mdb *db.MongoDB
func Init() {
mdb = db.Init(models.PaymentGuide)
initIndexes()
}
func guideColl(t *db.MongoTool) *db.MongoTool {
if t == nil {
return mdb.Coll(models.PaymentGuide)
}
return t.Coll(models.PaymentGuide)
}
func impressionColl() *db.MongoTool {
return mdb.Coll(models.PaymentGuideImpression)
}
func activeGuideIndexKeys() bson.D {
return bson.D{
{Key: "scene", Value: 1},
{Key: "enable", Value: 1},
{Key: "sort", Value: -1},
{Key: "updatedAt", Value: -1},
{Key: "_id", Value: -1},
}
}
func initIndexes() {
if _, err := guideColl(nil).CreateIndex([]mongo.IndexModel{
{
Keys: bson.D{
{Key: "scene", Value: 1},
{Key: "enable", Value: 1},
{Key: "sort", Value: -1},
{Key: "startAt", Value: 1},
{Key: "endAt", Value: 1},
},
},
{
Keys: activeGuideIndexKeys(),
},
}); err != nil {
panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.PaymentGuide, err))
}
//if err := impressionColl().DropIndexIfExists("uid_1_configId_1_scene_1"); err != nil {
// panic(fmt.Sprintf("%s model drop legacy index err ==>[%+v]", models.PaymentGuideImpression, err))
//}
if _, err := impressionColl().CreateIndex([]mongo.IndexModel{
{
Keys: bson.D{
{Key: "uid", Value: 1},
{Key: "configId", Value: 1},
{Key: "scene", Value: 1},
{Key: "contentVersion", Value: 1},
},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "requestId", Value: 1}},
},
}); err != nil {
panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.PaymentGuideImpression, err))
}
}
func Insert(p *PaymentGuide) error {
p.Normalize()
if err := p.Validate(); err != nil {
return err
}
now := time.Now()
p.ID = primitive.NewObjectID()
p.CreatedAt = now
p.UpdatedAt = now
_, err := guideColl(nil).InsertOne(p)
return err
}
// InsertMany validates every configuration before writing and inserts the
// bounded batch in one transaction so callers never observe a partial batch.
func InsertMany(ctx context.Context, configs []PaymentGuide) error {
if len(configs) == 0 {
return fmt.Errorf("payment guide configs are required")
}
if len(configs) > MaxBatchSceneCount {
return fmt.Errorf("payment guide configs cannot contain more than %d entries", MaxBatchSceneCount)
}
now := time.Now()
for i := range configs {
configs[i].Normalize()
if err := configs[i].Validate(); err != nil {
return fmt.Errorf("config %d: %w", i, err)
}
configs[i].ID = primitive.NewObjectID()
configs[i].CreatedAt = now
configs[i].UpdatedAt = now
}
// Use the request context and disable the legacy in-place transaction
// re-entry. A transient error is returned instead of rerunning writes in an
// already-started transaction, and cancelled requests stop database work.
return mdb.TransCtx(ctx, func(t *db.MongoTool) error {
_, err := guideColl(t).InsertMany(configs)
return err
}, (&db.TransOpts{}).SetReEntry(0))
}
func Update(p *PaymentGuide) error {
if p.ID.IsZero() {
return fmt.Errorf("id is required")
}
p.Normalize()
if err := p.Validate(); err != nil {
return err
}
p.UpdatedAt = time.Now()
set := bson.M{
"scene": p.Scene,
"segments": p.Segments,
"style": p.Style,
"title": p.Title,
"description": p.Description,
"cover": p.Cover,
"videoIds": p.VideoIDs,
"videoLimit": p.VideoLimit,
"productId": p.ProductID,
"durationSeconds": p.DurationSeconds,
"action": p.Action,
"enable": p.Enable,
"sort": p.Sort,
"updatedAt": p.UpdatedAt,
}
update := bson.M{"$set": set}
unset := bson.M{}
if p.StartAt == nil {
unset["startAt"] = ""
} else {
set["startAt"] = *p.StartAt
}
if p.EndAt == nil {
unset["endAt"] = ""
} else {
set["endAt"] = *p.EndAt
}
if len(unset) > 0 {
update["$unset"] = unset
}
result, err := guideColl(nil).UpdateOne(bson.M{"_id": p.ID}, update)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return mongo.ErrNoDocuments
}
return nil
}
func Delete(id primitive.ObjectID) error {
if id.IsZero() {
return fmt.Errorf("id is required")
}
_, err := guideColl(nil).DeleteById(id)
return err
}
func GetByID(id primitive.ObjectID) (PaymentGuide, error) {
var out PaymentGuide
err := guideColl(nil).FindOne(&out, bson.M{"_id": id})
return out, err
}
func List(scene string, skip, limit int64) ([]PaymentGuide, int64, bool, error) {
filter := paymentGuideListFilter(scene)
total, err := guideColl(nil).Count(filter)
if err != nil {
return nil, 0, false, err
}
var list []PaymentGuide
err = guideColl(nil).Find(&list, filter, options.Find().
SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}, {Key: "_id", Value: -1}}).
SetSkip(skip).
SetLimit(limit+1))
if err != nil {
return nil, 0, false, err
}
hasNext := len(list) > int(limit)
if hasNext {
list = list[:limit]
}
return list, total, hasNext, nil
}
func paymentGuideListFilter(scene string) bson.M {
filter := bson.M{"scene": bson.M{"$in": ConfigurableScenes()}}
if scene != "" {
filter["scene"] = scene
}
return filter
}
func FindActive(scene, segment string, now time.Time) (*PaymentGuide, error) {
configs, err := FindActiveByScenes([]string{scene}, segment, now)
if err != nil {
return nil, err
}
config, exists := configs[strings.ToUpper(strings.TrimSpace(scene))]
if !exists {
return nil, nil
}
return &config, nil
}
// FindActiveByScenes returns at most one highest-priority active configuration
// for every requested scene. The scene batch is deliberately bounded by the
// supported-scene count so Ping never fans out into per-scene database reads.
func FindActiveByScenes(scenes []string, segment string, now time.Time) (map[string]PaymentGuide, error) {
normalizedScenes, err := normalizeSceneBatch(scenes)
if err != nil {
return nil, err
}
result := make(map[string]PaymentGuide, len(normalizedScenes))
if len(normalizedScenes) == 0 {
return result, nil
}
segment = strings.ToUpper(strings.TrimSpace(segment))
segmentFilters := bson.A{
bson.M{"segments": bson.M{"$exists": false}},
bson.M{"segments": nil},
bson.M{"segments": bson.M{"$size": 0}},
}
if segment != "" {
segmentFilters = append(segmentFilters, bson.M{"segments": segment})
}
filter := bson.M{
"scene": bson.M{"$in": normalizedScenes},
"enable": true,
"$and": bson.A{
bson.M{"$or": bson.A{
bson.M{"startAt": bson.M{"$exists": false}},
bson.M{"startAt": nil},
bson.M{"startAt": bson.M{"$lte": now}},
}},
bson.M{"$or": bson.A{
bson.M{"endAt": bson.M{"$exists": false}},
bson.M{"endAt": nil},
bson.M{"endAt": bson.M{"$gt": now}},
}},
bson.M{"$or": segmentFilters},
},
}
pipeline := []bson.M{
{"$match": filter},
{"$sort": bson.D{
{Key: "scene", Value: 1},
{Key: "sort", Value: -1},
{Key: "updatedAt", Value: -1},
{Key: "_id", Value: -1},
}},
{"$group": bson.D{
{Key: "_id", Value: "$scene"},
{Key: "config", Value: bson.D{{Key: "$first", Value: "$$ROOT"}}},
}},
{"$replaceRoot": bson.D{{Key: "newRoot", Value: "$config"}}},
}
var configs []PaymentGuide
aggregateOpts := options.Aggregate().
SetHint(activeGuideIndexKeys()).
SetMaxTime(2 * time.Second)
if err = guideColl(nil).Aggregate(&configs, pipeline, aggregateOpts); err != nil {
return nil, err
}
for _, config := range configs {
result[config.Scene] = config
}
return result, nil
}
func normalizeSceneBatch(scenes []string) ([]string, error) {
if len(scenes) > MaxBatchSceneCount {
return nil, fmt.Errorf("scenes cannot contain more than %d entries", MaxBatchSceneCount)
}
seen := make(map[string]struct{}, len(scenes))
normalized := make([]string, 0, len(scenes))
for _, scene := range scenes {
scene = strings.ToUpper(strings.TrimSpace(scene))
if !ValidScene(scene) {
return nil, fmt.Errorf("unsupported scene: %s", scene)
}
if _, exists := seen[scene]; exists {
continue
}
seen[scene] = struct{}{}
normalized = append(normalized, scene)
}
return normalized, nil
}
func HasImpression(uid uint64, configID primitive.ObjectID, scene, contentVersion string) (bool, error) {
count, err := impressionColl().Count(bson.M{
"uid": uid,
"configId": configID,
"scene": scene,
"contentVersion": contentVersion,
})
return count > 0, err
}
// FindImpressionConfigIDs performs one bounded lookup for the ordinary Ping
// scenes and returns the configurations already shown to the user.
func FindImpressionConfigIDs(uid uint64, configIDs []primitive.ObjectID) (map[primitive.ObjectID]bool, error) {
result := make(map[primitive.ObjectID]bool, len(configIDs))
if uid == 0 || len(configIDs) == 0 {
return result, nil
}
if len(configIDs) > MaxBatchSceneCount {
return nil, fmt.Errorf("configIds cannot contain more than %d entries", MaxBatchSceneCount)
}
seen := make(map[primitive.ObjectID]struct{}, len(configIDs))
uniqueIDs := make([]primitive.ObjectID, 0, len(configIDs))
for _, configID := range configIDs {
if configID.IsZero() {
return nil, fmt.Errorf("configId cannot be empty")
}
if _, exists := seen[configID]; exists {
continue
}
seen[configID] = struct{}{}
uniqueIDs = append(uniqueIDs, configID)
}
type impressionRef struct {
ConfigID primitive.ObjectID `bson:"configId"`
}
var rows []impressionRef
findOpts := options.Find().
SetProjection(bson.M{"_id": 0, "configId": 1}).
SetLimit(int64(len(uniqueIDs)))
if err := impressionColl().Find(&rows, bson.M{
"uid": uid,
"configId": bson.M{"$in": uniqueIDs},
"contentVersion": "",
}, findOpts); err != nil {
return nil, err
}
for _, row := range rows {
result[row.ConfigID] = true
}
return result, nil
}
func RecordImpression(item Impression) error {
item.ID = primitive.NilObjectID
item.CreatedAt = time.Now()
_, err := impressionColl().UpsertOne(
bson.M{
"uid": item.UID,
"configId": item.ConfigID,
"scene": item.Scene,
"contentVersion": item.ContentVersion,
},
bson.M{"$setOnInsert": item},
)
return err
}
+242
View File
@@ -0,0 +1,242 @@
package paymentguidemod
import (
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
// SceneHomeNewUser is kept only for backward compatibility with legacy
// records and clients. It is no longer configurable or returned to App/Web.
SceneHomeNewUser = "HOME_NEW_USER"
SceneHomeNewUserFreeTrial = "HOME_NEW_USER_FREE_TRIAL"
SceneHomeOldUser = "HOME_OLD_USER"
SceneVideoPreviewEnd = "VIDEO_PREVIEW_END"
SceneDiscountCountdown = "DISCOUNT_COUNTDOWN"
SceneVideoBack = "VIDEO_BACK"
SceneVIPCenter = "VIP_CENTER"
SceneVIPContentUpdate = "VIP_CONTENT_UPDATE"
)
const (
DefaultVIPContentVideoLimit int64 = 4
MaxVIPContentVideoLimit int64 = 20
MaxBatchSceneCount = 7
)
const (
SegmentNewNeverPaid = "NEW_NEVER_PAID"
SegmentOldNeverPaid = "OLD_NEVER_PAID"
SegmentPaidUpgrade = "PAID_UPGRADE"
SegmentMaxVIP = "MAX_VIP"
SegmentNormal = "NORMAL"
SegmentUnregistered = "UNREGISTERED"
)
var validScenes = map[string]struct{}{
SceneHomeNewUser: {},
SceneHomeNewUserFreeTrial: {},
SceneHomeOldUser: {},
SceneVideoPreviewEnd: {},
SceneDiscountCountdown: {},
SceneVideoBack: {},
SceneVIPCenter: {},
SceneVIPContentUpdate: {},
}
var configurableScenes = []string{
SceneHomeNewUserFreeTrial,
SceneHomeOldUser,
SceneVideoPreviewEnd,
SceneDiscountCountdown,
SceneVideoBack,
SceneVIPCenter,
SceneVIPContentUpdate,
}
var validSegments = map[string]struct{}{
SegmentNewNeverPaid: {},
SegmentOldNeverPaid: {},
SegmentPaidUpgrade: {},
SegmentMaxVIP: {},
SegmentNormal: {},
SegmentUnregistered: {},
}
type Action struct {
Type string `json:"type" bson:"type"`
Value string `json:"value" bson:"value"`
}
type PaymentGuide struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Scene string `json:"scene" bson:"scene"`
Segments []string `json:"segments" bson:"segments"`
Style string `json:"style" bson:"style"`
Title string `json:"title" bson:"title"`
Description string `json:"description" bson:"description"`
Cover string `json:"cover" bson:"cover"`
VideoIDs []string `json:"videoIds" bson:"videoIds"`
VideoLimit int64 `json:"videoLimit,omitempty" bson:"videoLimit,omitempty"`
ProductID string `json:"productId" bson:"productId"`
DurationSeconds int64 `json:"durationSeconds" bson:"durationSeconds"`
Action Action `json:"action" bson:"action"`
Enable bool `json:"enable" bson:"enable"`
Sort int `json:"sort" bson:"sort"`
StartAt *time.Time `json:"startAt,omitempty" bson:"startAt,omitempty"`
EndAt *time.Time `json:"endAt,omitempty" bson:"endAt,omitempty"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
}
type Impression struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
UID uint64 `json:"uid" bson:"uid"`
ConfigID primitive.ObjectID `json:"configId" bson:"configId"`
Scene string `json:"scene" bson:"scene"`
ContentVersion string `json:"contentVersion" bson:"contentVersion"`
VideoID string `json:"videoId" bson:"videoId"`
RequestID string `json:"requestId" bson:"requestId"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
}
func ValidScene(scene string) bool {
_, ok := validScenes[scene]
return ok
}
// ConfigurableScenes returns the bounded scene list exposed to App/Web. The
// legacy HOME_NEW_USER code remains valid for old data but is intentionally
// absent from this list.
func ConfigurableScenes() []string {
return append([]string(nil), configurableScenes...)
}
func ConfigurableScene(scene string) bool {
for _, candidate := range configurableScenes {
if scene == candidate {
return true
}
}
return false
}
func (p *PaymentGuide) Normalize() {
p.Scene = strings.TrimSpace(strings.ToUpper(p.Scene))
p.Style = strings.TrimSpace(strings.ToUpper(p.Style))
p.Title = strings.TrimSpace(p.Title)
p.ProductID = strings.TrimSpace(p.ProductID)
p.Action.Type = strings.TrimSpace(strings.ToUpper(p.Action.Type))
p.Action.Value = strings.TrimSpace(p.Action.Value)
if p.Action.Type == "VIP_PRODUCT" {
if p.ProductID == "" {
p.ProductID = p.Action.Value
}
if p.Action.Value == "" {
p.Action.Value = p.ProductID
}
}
seen := make(map[string]struct{}, len(p.Segments))
segments := make([]string, 0, len(p.Segments))
for _, segment := range p.Segments {
segment = strings.TrimSpace(strings.ToUpper(segment))
if segment == "" {
continue
}
if _, exists := seen[segment]; exists {
continue
}
seen[segment] = struct{}{}
segments = append(segments, segment)
}
p.Segments = segments
}
func (p PaymentGuide) Validate() error {
if !ValidScene(p.Scene) {
return fmt.Errorf("unsupported scene: %s", p.Scene)
}
for _, segment := range p.Segments {
if _, ok := validSegments[segment]; !ok {
return fmt.Errorf("unsupported segment: %s", segment)
}
}
if p.Style == "" {
return fmt.Errorf("style is required")
}
if p.Title == "" {
return fmt.Errorf("title is required")
}
if p.DurationSeconds < 0 {
return fmt.Errorf("durationSeconds cannot be negative")
}
if p.VideoLimit < 0 || p.VideoLimit > MaxVIPContentVideoLimit {
return fmt.Errorf("videoLimit must be between 0 and %d", MaxVIPContentVideoLimit)
}
if p.StartAt != nil && p.EndAt != nil && !p.EndAt.After(*p.StartAt) {
return fmt.Errorf("endAt must be later than startAt")
}
switch p.Action.Type {
case "VIP_PRODUCT":
if p.Action.Value == "" {
return fmt.Errorf("action.value is required for %s", p.Action.Type)
}
if p.ProductID != p.Action.Value {
return fmt.Errorf("productId must match action.value for VIP_PRODUCT")
}
case "INTERNAL", "EXTERNAL":
if p.Action.Value == "" {
return fmt.Errorf("action.value is required for %s", p.Action.Type)
}
case "NONE":
if p.Action.Value != "" {
return fmt.Errorf("action.value must be empty for NONE")
}
default:
return fmt.Errorf("unsupported action.type: %s", p.Action.Type)
}
return nil
}
// EffectiveVideoLimit returns the configured video count with a safe default
// for legacy configurations that do not contain videoLimit.
func (p PaymentGuide) EffectiveVideoLimit() int64 {
if p.VideoLimit <= 0 {
return DefaultVIPContentVideoLimit
}
if p.VideoLimit > MaxVIPContentVideoLimit {
return MaxVIPContentVideoLimit
}
return p.VideoLimit
}
// VIPContentVideoLimit keeps the scene-specific call site readable while
// sharing the same defaulting rules used by the Web configuration page.
func (p PaymentGuide) VIPContentVideoLimit() int64 {
return p.EffectiveVideoLimit()
}
func (p PaymentGuide) ActiveAt(now time.Time) bool {
if !p.Enable {
return false
}
if p.StartAt != nil && p.StartAt.After(now) {
return false
}
return p.EndAt == nil || p.EndAt.After(now)
}
func (p PaymentGuide) MatchesSegment(segment string) bool {
if len(p.Segments) == 0 {
return true
}
for _, allowed := range p.Segments {
if allowed == segment {
return true
}
}
return false
}
+184
View File
@@ -0,0 +1,184 @@
package paymentguidemod
import (
"context"
"reflect"
"strings"
"testing"
"time"
"go.mongodb.org/mongo-driver/bson"
)
func TestActiveGuideIndexKeysMatchBatchQueryOrder(t *testing.T) {
want := bson.D{
{Key: "scene", Value: 1},
{Key: "enable", Value: 1},
{Key: "sort", Value: -1},
{Key: "updatedAt", Value: -1},
{Key: "_id", Value: -1},
}
if got := activeGuideIndexKeys(); !reflect.DeepEqual(got, want) {
t.Fatalf("activeGuideIndexKeys() = %v, want %v", got, want)
}
}
func TestPaymentGuideNormalizeAndValidate(t *testing.T) {
config := PaymentGuide{
Scene: "video_preview_end",
Segments: []string{"old_never_paid", "OLD_NEVER_PAID"},
Style: "bottom_sheet",
Title: "开通会员",
Action: Action{Type: "vip_product", Value: "product-id"},
}
config.Normalize()
if err := config.Validate(); err != nil {
t.Fatal(err)
}
if config.Scene != SceneVideoPreviewEnd || len(config.Segments) != 1 {
t.Fatalf("unexpected normalized config: %+v", config)
}
}
func TestPaymentGuideActiveAt(t *testing.T) {
now := time.Now()
start := now.Add(-time.Minute)
end := now.Add(time.Minute)
config := PaymentGuide{Enable: true, StartAt: &start, EndAt: &end}
if !config.ActiveAt(now) {
t.Fatal("config should be active")
}
}
func TestPaymentGuideVIPContentVideoLimit(t *testing.T) {
if DefaultVIPContentVideoLimit != 4 {
t.Fatalf("DefaultVIPContentVideoLimit = %d, want 4", DefaultVIPContentVideoLimit)
}
tests := []struct {
name string
limit int64
want int64
}{
{name: "legacy default", limit: 0, want: DefaultVIPContentVideoLimit},
{name: "minimum", limit: 1, want: 1},
{name: "configured", limit: 12, want: 12},
{name: "maximum", limit: MaxVIPContentVideoLimit, want: MaxVIPContentVideoLimit},
{name: "defensive clamp", limit: MaxVIPContentVideoLimit + 1, want: MaxVIPContentVideoLimit},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := PaymentGuide{VideoLimit: tt.limit}
if got := config.VIPContentVideoLimit(); got != tt.want {
t.Fatalf("VIPContentVideoLimit() = %d, want %d", got, tt.want)
}
})
}
}
func TestPaymentGuideBatchSceneLimitMatchesSupportedScenes(t *testing.T) {
if len(ConfigurableScenes()) != MaxBatchSceneCount {
t.Fatalf("len(ConfigurableScenes()) = %d, MaxBatchSceneCount = %d", len(ConfigurableScenes()), MaxBatchSceneCount)
}
}
func TestLegacyHomeNewUserSceneIsNotConfigurable(t *testing.T) {
if !ValidScene(SceneHomeNewUser) {
t.Fatal("legacy HOME_NEW_USER must remain recognizable")
}
if ConfigurableScene(SceneHomeNewUser) {
t.Fatal("legacy HOME_NEW_USER must not be returned as configurable")
}
if !ConfigurableScene(SceneHomeNewUserFreeTrial) {
t.Fatal("HOME_NEW_USER_FREE_TRIAL must be configurable")
}
}
func TestConfigurableScenesReturnsCopy(t *testing.T) {
scenes := ConfigurableScenes()
scenes[0] = "CHANGED"
if ConfigurableScenes()[0] != SceneHomeNewUserFreeTrial {
t.Fatal("ConfigurableScenes must not expose mutable package state")
}
}
func TestPaymentGuideListFilterExcludesLegacyScene(t *testing.T) {
filter := paymentGuideListFilter("")
sceneFilter, ok := filter["scene"].(bson.M)
if !ok {
t.Fatalf("unexpected scene filter: %#v", filter["scene"])
}
scenes, ok := sceneFilter["$in"].([]string)
if !ok {
t.Fatalf("unexpected scene inclusion filter: %#v", sceneFilter["$in"])
}
for _, scene := range scenes {
if scene == SceneHomeNewUser {
t.Fatalf("legacy HOME_NEW_USER must not be listed: %#v", scenes)
}
}
if filter := paymentGuideListFilter(SceneVIPCenter); filter["scene"] != SceneVIPCenter {
t.Fatalf("explicit scene filter was not preserved: %#v", filter)
}
}
func TestDiscountCountdownIsSupportedScene(t *testing.T) {
if !ValidScene(SceneDiscountCountdown) {
t.Fatal("DISCOUNT_COUNTDOWN must be a supported payment-guide scene")
}
}
func TestPaymentGuideInsertManyRejectsUnboundedBatchesBeforeDatabaseAccess(t *testing.T) {
tests := []struct {
name string
configs []PaymentGuide
match string
}{
{name: "empty", configs: nil, match: "required"},
{name: "too many", configs: make([]PaymentGuide, MaxBatchSceneCount+1), match: "more than"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InsertMany(context.Background(), tt.configs)
if err == nil || !strings.Contains(err.Error(), tt.match) {
t.Fatalf("InsertMany() error = %v, want match %q", err, tt.match)
}
})
}
}
func TestNormalizeSceneBatch(t *testing.T) {
scenes, err := normalizeSceneBatch([]string{" home_new_user ", "HOME_NEW_USER", "vip_center"})
if err != nil {
t.Fatal(err)
}
if len(scenes) != 2 || scenes[0] != SceneHomeNewUser || scenes[1] != SceneVIPCenter {
t.Fatalf("normalizeSceneBatch() = %#v", scenes)
}
if _, err = normalizeSceneBatch([]string{"UNKNOWN"}); err == nil {
t.Fatal("normalizeSceneBatch must reject unsupported scenes")
}
if _, err = normalizeSceneBatch(make([]string, MaxBatchSceneCount+1)); err == nil {
t.Fatal("normalizeSceneBatch must reject an unbounded batch")
}
}
func TestPaymentGuideValidateVideoLimit(t *testing.T) {
valid := PaymentGuide{
Scene: SceneVIPContentUpdate,
Style: "BOTTOM_SHEET",
Title: "VIP内容更新",
Action: Action{Type: "NONE"},
VideoLimit: MaxVIPContentVideoLimit,
}
if err := valid.Validate(); err != nil {
t.Fatalf("Validate() unexpected error: %v", err)
}
for _, limit := range []int64{-1, MaxVIPContentVideoLimit + 1} {
invalid := valid
invalid.VideoLimit = limit
if err := invalid.Validate(); err == nil {
t.Fatalf("Validate() with videoLimit %d should fail", limit)
}
}
}