382 lines
10 KiB
Go
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
|
|
}
|