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 }