Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

382 lines
10 KiB
Go

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
}