@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user