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
@@ -0,0 +1,168 @@
package paymentguideser
import (
"context"
"fmt"
"strings"
"time"
"91porn-server/models/cache/sysconfdata"
"91porn-server/models/v/paymentguidemod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ListReq struct {
Scene string `form:"scene"`
PageNumber int64 `form:"pageNumber,default=1" binding:"min=1"`
PageSize int64 `form:"pageSize,default=20" binding:"min=1,max=100"`
}
type ListResp struct {
Total int64 `json:"total"`
HasNext bool `json:"hasNext"`
List []paymentguidemod.PaymentGuide `json:"list"`
SceneOptions []SceneOption `json:"sceneOptions"`
}
type SceneOption struct {
Label string `json:"label"`
Value string `json:"value"`
TotalWatchCount *uint64 `json:"totalWatchCount,omitempty"`
}
// AddReq keeps the existing flat single-scene request compatible and adds an
// optional batch envelope for applying one configuration to multiple scenes.
type AddReq struct {
paymentguidemod.PaymentGuide
Scenes []string `json:"scenes,omitempty"`
Config *paymentguidemod.PaymentGuide `json:"config,omitempty"`
}
type BatchAddReq struct {
Scenes []string `json:"scenes"`
Config paymentguidemod.PaymentGuide `json:"config"`
}
type BatchAddItem struct {
Scene string `json:"scene"`
ID string `json:"id"`
}
type BatchAddResp struct {
List []BatchAddItem `json:"list"`
}
func (p AddReq) IsBatch() bool {
return p.Config != nil || p.Scenes != nil
}
func (p *ListReq) List() (ListResp, error) {
p.Scene = strings.ToUpper(strings.TrimSpace(p.Scene))
if p.Scene != "" && !paymentguidemod.ConfigurableScene(p.Scene) {
return ListResp{}, fmt.Errorf("unsupported scene: %s", p.Scene)
}
list, total, hasNext, err := paymentguidemod.List(
p.Scene,
(p.PageNumber-1)*p.PageSize,
p.PageSize,
)
if list == nil {
list = []paymentguidemod.PaymentGuide{}
}
normalizeVideoLimits(list)
return ListResp{
Total: total,
HasNext: hasNext,
List: list,
SceneOptions: sceneOptions(sysconfdata.GetTotalWatchCount()),
}, err
}
func sceneOptions(totalWatchCount uint64) []SceneOption {
return []SceneOption{
{Label: fmt.Sprintf("首页新用户免费%d次试看", totalWatchCount), Value: paymentguidemod.SceneHomeNewUserFreeTrial, TotalWatchCount: &totalWatchCount},
{Label: "首页老用户", Value: paymentguidemod.SceneHomeOldUser},
{Label: "视频试看3秒", Value: paymentguidemod.SceneVideoPreviewEnd},
{Label: "优惠倒计时", Value: paymentguidemod.SceneDiscountCountdown},
{Label: "视频返回", Value: paymentguidemod.SceneVideoBack},
{Label: "VIP中心", Value: paymentguidemod.SceneVIPCenter},
{Label: "VIP内容更新", Value: paymentguidemod.SceneVIPContentUpdate},
}
}
func normalizeVideoLimits(list []paymentguidemod.PaymentGuide) {
for i := range list {
if list[i].Scene == paymentguidemod.SceneVIPContentUpdate {
list[i].VideoLimit = list[i].EffectiveVideoLimit()
}
}
}
func Add(config *paymentguidemod.PaymentGuide) error {
config.Normalize()
if !paymentguidemod.ConfigurableScene(config.Scene) {
return fmt.Errorf("unsupported scene: %s", config.Scene)
}
return paymentguidemod.Insert(config)
}
func (p BatchAddReq) Configs() ([]paymentguidemod.PaymentGuide, error) {
if len(p.Scenes) == 0 {
return nil, fmt.Errorf("scenes are required")
}
if len(p.Scenes) > paymentguidemod.MaxBatchSceneCount {
return nil, fmt.Errorf("scenes cannot contain more than %d entries", paymentguidemod.MaxBatchSceneCount)
}
seen := make(map[string]struct{}, len(p.Scenes))
configs := make([]paymentguidemod.PaymentGuide, 0, len(p.Scenes))
for _, rawScene := range p.Scenes {
scene := strings.ToUpper(strings.TrimSpace(rawScene))
if !paymentguidemod.ConfigurableScene(scene) {
return nil, fmt.Errorf("unsupported scene: %s", scene)
}
if _, ok := seen[scene]; ok {
return nil, fmt.Errorf("duplicate scene: %s", scene)
}
seen[scene] = struct{}{}
config := p.Config
config.ID = primitive.NilObjectID
config.Scene = scene
config.Segments = append([]string(nil), p.Config.Segments...)
config.VideoIDs = append([]string(nil), p.Config.VideoIDs...)
if config.VideoLimit == 0 {
config.VideoLimit = paymentguidemod.DefaultVIPContentVideoLimit
}
config.CreatedAt = time.Time{}
config.UpdatedAt = time.Time{}
config.Normalize()
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("scene %s: %w", scene, err)
}
configs = append(configs, config)
}
return configs, nil
}
func BatchAdd(ctx context.Context, configs []paymentguidemod.PaymentGuide) (BatchAddResp, error) {
if err := paymentguidemod.InsertMany(ctx, configs); err != nil {
return BatchAddResp{}, err
}
items := make([]BatchAddItem, 0, len(configs))
for i := range configs {
items = append(items, BatchAddItem{Scene: configs[i].Scene, ID: configs[i].ID.Hex()})
}
return BatchAddResp{List: items}, nil
}
func Edit(config *paymentguidemod.PaymentGuide) error {
config.Normalize()
if !paymentguidemod.ConfigurableScene(config.Scene) {
return fmt.Errorf("unsupported scene: %s", config.Scene)
}
return paymentguidemod.Update(config)
}
func Delete(id primitive.ObjectID) error {
return paymentguidemod.Delete(id)
}
@@ -0,0 +1,258 @@
package paymentguideser
import (
"encoding/json"
"strings"
"testing"
"time"
"91porn-server/models/v/paymentguidemod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestPaymentGuideListResponseUsesLegacyVideoLimitDefault(t *testing.T) {
list := []paymentguidemod.PaymentGuide{
{Scene: paymentguidemod.SceneVIPContentUpdate},
{Scene: paymentguidemod.SceneVIPContentUpdate, VideoLimit: 12},
{Scene: paymentguidemod.SceneVIPCenter},
}
normalizeVideoLimits(list)
if list[0].VideoLimit != paymentguidemod.DefaultVIPContentVideoLimit {
t.Fatalf("legacy videoLimit = %d, want %d", list[0].VideoLimit, paymentguidemod.DefaultVIPContentVideoLimit)
}
if list[1].VideoLimit != 12 {
t.Fatalf("configured videoLimit = %d, want 12", list[1].VideoLimit)
}
if list[2].VideoLimit != 0 {
t.Fatalf("unrelated scene videoLimit = %d, want 0", list[2].VideoLimit)
}
}
func TestSceneOptionsUseConfiguredFreeTrialCountAndHideLegacyScene(t *testing.T) {
options := sceneOptions(5)
if len(options) != paymentguidemod.MaxBatchSceneCount {
t.Fatalf("len(options) = %d, want %d", len(options), paymentguidemod.MaxBatchSceneCount)
}
if options[0].Value != paymentguidemod.SceneHomeNewUserFreeTrial ||
options[0].Label != "首页新用户免费5次试看" ||
options[0].TotalWatchCount == nil || *options[0].TotalWatchCount != 5 {
t.Fatalf("unexpected free-trial option: %+v", options[0])
}
for _, option := range options {
if option.Value == paymentguidemod.SceneHomeNewUser {
t.Fatalf("legacy HOME_NEW_USER must not be returned: %+v", options)
}
}
}
func TestWebConfigurationRejectsLegacyHomeNewUserScene(t *testing.T) {
listReq := ListReq{Scene: paymentguidemod.SceneHomeNewUser, PageNumber: 1, PageSize: 20}
if _, err := listReq.List(); err == nil || !strings.Contains(err.Error(), "unsupported scene") {
t.Fatalf("List() error = %v, want unsupported scene", err)
}
config := validBatchConfig()
config.Scene = paymentguidemod.SceneHomeNewUser
if err := Add(&config); err == nil || !strings.Contains(err.Error(), "unsupported scene") {
t.Fatalf("Add() error = %v, want unsupported scene", err)
}
}
func validBatchConfig() paymentguidemod.PaymentGuide {
return paymentguidemod.PaymentGuide{
Segments: []string{paymentguidemod.SegmentNormal},
Style: "BOTTOM_SHEET",
Title: "会员内容更新",
VideoIDs: []string{"video-a"},
Action: paymentguidemod.Action{Type: "NONE"},
Enable: true,
Sort: 100,
}
}
func TestBatchAddReqConfigsNormalizesScenesAndDefaultsVideoLimit(t *testing.T) {
req := BatchAddReq{
Scenes: []string{" home_new_user_free_trial ", "vip_content_update"},
Config: validBatchConfig(),
}
configs, err := req.Configs()
if err != nil {
t.Fatal(err)
}
if len(configs) != 2 {
t.Fatalf("len(configs) = %d, want 2", len(configs))
}
if configs[0].Scene != paymentguidemod.SceneHomeNewUserFreeTrial || configs[1].Scene != paymentguidemod.SceneVIPContentUpdate {
t.Fatalf("unexpected scenes: %#v", []string{configs[0].Scene, configs[1].Scene})
}
for i := range configs {
if configs[i].VideoLimit != paymentguidemod.DefaultVIPContentVideoLimit {
t.Fatalf("configs[%d].VideoLimit = %d, want %d", i, configs[i].VideoLimit, paymentguidemod.DefaultVIPContentVideoLimit)
}
}
configs[0].Segments[0] = paymentguidemod.SegmentMaxVIP
configs[0].VideoIDs[0] = "changed"
if configs[1].Segments[0] != paymentguidemod.SegmentNormal || configs[1].VideoIDs[0] != "video-a" {
t.Fatal("batch configs must not share slice storage")
}
if req.Config.Segments[0] != paymentguidemod.SegmentNormal || req.Config.VideoIDs[0] != "video-a" {
t.Fatal("building configs must not mutate the request template")
}
}
func TestBatchAddReqConfigsPreservesExplicitVideoLimit(t *testing.T) {
config := validBatchConfig()
config.VideoLimit = 12
configs, err := (BatchAddReq{
Scenes: []string{paymentguidemod.SceneVIPContentUpdate},
Config: config,
}).Configs()
if err != nil {
t.Fatal(err)
}
if configs[0].VideoLimit != 12 {
t.Fatalf("VideoLimit = %d, want 12", configs[0].VideoLimit)
}
}
func TestBatchAddReqConfigsSupportsEveryScene(t *testing.T) {
configs, err := (BatchAddReq{
Scenes: []string{
paymentguidemod.SceneHomeNewUserFreeTrial,
paymentguidemod.SceneHomeOldUser,
paymentguidemod.SceneVideoPreviewEnd,
paymentguidemod.SceneDiscountCountdown,
paymentguidemod.SceneVideoBack,
paymentguidemod.SceneVIPCenter,
paymentguidemod.SceneVIPContentUpdate,
},
Config: validBatchConfig(),
}).Configs()
if err != nil {
t.Fatal(err)
}
if len(configs) != paymentguidemod.MaxBatchSceneCount {
t.Fatalf("len(configs) = %d, want %d", len(configs), paymentguidemod.MaxBatchSceneCount)
}
}
func TestBatchAddReqConfigsDiscardsTemplateIdentityAndTimestamps(t *testing.T) {
config := validBatchConfig()
config.ID = primitive.NewObjectID()
config.Scene = paymentguidemod.SceneVIPCenter
config.CreatedAt = time.Now().Add(-2 * time.Hour)
config.UpdatedAt = time.Now().Add(-time.Hour)
configs, err := (BatchAddReq{
Scenes: []string{paymentguidemod.SceneHomeNewUserFreeTrial},
Config: config,
}).Configs()
if err != nil {
t.Fatal(err)
}
if !configs[0].ID.IsZero() {
t.Fatalf("ID = %s, want zero before insertion", configs[0].ID.Hex())
}
if configs[0].Scene != paymentguidemod.SceneHomeNewUserFreeTrial {
t.Fatalf("Scene = %s, want %s", configs[0].Scene, paymentguidemod.SceneHomeNewUserFreeTrial)
}
if !configs[0].CreatedAt.IsZero() || !configs[0].UpdatedAt.IsZero() {
t.Fatalf("timestamps must be zero before insertion: %+v", configs[0])
}
}
func TestBatchAddReqConfigsRejectsInvalidScenes(t *testing.T) {
tests := []struct {
name string
scenes []string
match string
}{
{name: "empty", scenes: nil, match: "scenes are required"},
{name: "unsupported", scenes: []string{"UNKNOWN"}, match: "unsupported scene"},
{name: "duplicate after normalization", scenes: []string{"VIP_CENTER", " vip_center "}, match: "duplicate scene"},
{name: "too many inputs", scenes: []string{"a", "b", "c", "d", "e", "f", "g", "h"}, match: "more than 7"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := (BatchAddReq{Scenes: tt.scenes, Config: validBatchConfig()}).Configs()
if err == nil || !strings.Contains(err.Error(), tt.match) {
t.Fatalf("Configs() error = %v, want match %q", err, tt.match)
}
})
}
}
func TestBatchAddReqConfigsValidatesTemplateForEveryScene(t *testing.T) {
config := validBatchConfig()
config.Title = ""
_, err := (BatchAddReq{
Scenes: []string{paymentguidemod.SceneVIPCenter},
Config: config,
}).Configs()
if err == nil || !strings.Contains(err.Error(), "title is required") {
t.Fatalf("Configs() error = %v, want title validation error", err)
}
}
func TestAddReqSupportsExistingSingleSceneJSON(t *testing.T) {
body := []byte(`{
"scene":"VIP_CONTENT_UPDATE",
"style":"BOTTOM_SHEET",
"title":"会员内容更新",
"action":{"type":"NONE","value":""}
}`)
var req AddReq
if err := json.Unmarshal(body, &req); err != nil {
t.Fatal(err)
}
if req.IsBatch() {
t.Fatal("existing flat add request must remain single-scene")
}
if req.Scene != paymentguidemod.SceneVIPContentUpdate || req.Title != "会员内容更新" {
t.Fatalf("unexpected single add request: %+v", req.PaymentGuide)
}
}
func TestAddReqSupportsBatchJSONShape(t *testing.T) {
body := []byte(`{
"scenes":["HOME_NEW_USER_FREE_TRIAL","VIP_CONTENT_UPDATE"],
"config":{
"style":"BOTTOM_SHEET",
"title":"会员内容更新",
"videoLimit":20,
"action":{"type":"NONE","value":""},
"enable":true,
"sort":100
}
}`)
var req AddReq
if err := json.Unmarshal(body, &req); err != nil {
t.Fatal(err)
}
if !req.IsBatch() || req.Config == nil {
t.Fatalf("unexpected batch add request: %+v", req)
}
configs, err := (BatchAddReq{Scenes: req.Scenes, Config: *req.Config}).Configs()
if err != nil {
t.Fatal(err)
}
if len(configs) != 2 || configs[1].VideoLimit != 20 {
t.Fatalf("unexpected configs: %+v", configs)
}
}
func TestAddReqTreatsPresentEmptyScenesAsBatch(t *testing.T) {
var req AddReq
if err := json.Unmarshal([]byte(`{"scenes":[]}`), &req); err != nil {
t.Fatal(err)
}
if !req.IsBatch() {
t.Fatal("a present scenes field must use batch validation")
}
_, err := (BatchAddReq{Scenes: req.Scenes, Config: paymentguidemod.PaymentGuide{}}).Configs()
if err == nil || !strings.Contains(err.Error(), "scenes are required") {
t.Fatalf("Configs() error = %v, want scenes validation error", err)
}
}