package vipcardexperimentser import ( "fmt" "strings" "time" "91porn-server/app/proto" "91porn-server/models/v/vipcardexperimentmod" "go.mongodb.org/mongo-driver/bson/primitive" ) const maxEventsPerRequest = 50 type EventInput struct { EventID string `json:"eventId"` EventName string `json:"eventName"` SessionID string `json:"sessionId"` OccurredAt time.Time `json:"occurredAt"` ExperimentID string `json:"experimentId"` Variant string `json:"variant"` ProductID primitive.ObjectID `json:"productId"` } type EventsRequest struct { Events []EventInput `json:"events" binding:"required"` } type EventsResponse struct { Accepted int `json:"accepted"` Duplicated int `json:"duplicated"` } // ApplyToProductResponse applies the active experiment to the normal product response. // No experiment or an unusable configuration keeps the legacy response unchanged. func ApplyToProductResponse(uid uint64, response *proto.ProductRes, now time.Time) error { if response == nil { return nil } response.ExperimentStatus = vipcardexperimentmod.StatusDisabled if uid == 0 { return nil } experiment, err := vipcardexperimentmod.Current(now) if err != nil || experiment == nil { return err } variant := experiment.Assign(uid) config, ok := experiment.ConfigFor(variant) if !ok { return nil } if !applyExperiment(response, experiment.ExperimentID, variant, config) { return nil } otherConfig := experiment.VariantA if variant == vipcardexperimentmod.VariantA { otherConfig = experiment.VariantB } filterOtherVariantProducts(response, config.ProductIDs, otherConfig.ProductIDs) return nil } func applyExperiment( response *proto.ProductRes, experimentID string, variant string, config vipcardexperimentmod.VariantConfig, ) bool { if !applyVariant(response, config) { return false } response.ExperimentID = experimentID response.ExperimentStatus = vipcardexperimentmod.StatusActive response.Variant = variant response.SkinKey = config.SkinKey if !config.DefaultProductID.IsZero() { response.DefaultProductID = config.DefaultProductID.Hex() } response.UIConfig = toResponseUIConfig(config.UIConfig) return true } func toResponseUIConfig(config *vipcardexperimentmod.UIConfig) *proto.VIPCardUIConfig { if config == nil { return nil } response := &proto.VIPCardUIConfig{ BackgroundImage: config.BackgroundImage, BadgeStyles: make([]proto.VIPCardBadgeStyle, len(config.BadgeStyles)), } for i, style := range config.BadgeStyles { response.BadgeStyles[i] = proto.VIPCardBadgeStyle{ BadgeType: style.BadgeType, BackgroundColor: style.BackgroundColor, TextColor: style.TextColor, } } return response } func applyVariant(response *proto.ProductRes, config vipcardexperimentmod.VariantConfig) bool { productByID := make(map[primitive.ObjectID]proto.VIPListRes) positionByID := make(map[primitive.ObjectID]int) for positionIndex := range response.List { for _, product := range response.List[positionIndex].List { productByID[product.ID] = product positionByID[product.ID] = positionIndex } } badges := make(map[primitive.ObjectID]vipcardexperimentmod.ProductBadge, len(config.ProductBadges)) for _, badge := range config.ProductBadges { badges[badge.ProductID] = badge } affectedPositions := make(map[int]struct{}) orderedPositionIndexes := make([]int, 0, len(response.List)) selectedByPosition := make(map[int][]proto.VIPListRes) defaultVisible := config.DefaultProductID.IsZero() for productOrder, productID := range config.ProductIDs { product, exists := productByID[productID] if !exists { continue } product.Sort = productOrder + 1 positionIndex := positionByID[productID] if _, affected := affectedPositions[positionIndex]; !affected { affectedPositions[positionIndex] = struct{}{} orderedPositionIndexes = append(orderedPositionIndexes, positionIndex) } if badge, exists := badges[productID]; exists { product.BadgeType = badge.BadgeType product.BadgeText = badge.BadgeText } if productID == config.DefaultProductID { defaultVisible = true } selectedByPosition[positionIndex] = append(selectedByPosition[positionIndex], product) } if len(affectedPositions) == 0 { return false } if !defaultVisible { return false } firstAffectedPosition := len(response.List) for positionIndex := range affectedPositions { if positionIndex < firstAffectedPosition { firstAffectedPosition = positionIndex } } orderedPositions := make([]proto.ProductList, 0, len(orderedPositionIndexes)) for _, positionIndex := range orderedPositionIndexes { position := response.List[positionIndex] position.List = selectedByPosition[positionIndex] orderedPositions = append(orderedPositions, position) } positions := make([]proto.ProductList, 0, len(response.List)) for positionIndex, position := range response.List { if positionIndex == firstAffectedPosition { positions = append(positions, orderedPositions...) } if _, affected := affectedPositions[positionIndex]; affected { continue } positions = append(positions, position) } response.List = positions return true } // filterOtherVariantProducts removes products configured exclusively for the // other experiment variant while preserving positions that do not participate // in the experiment. This keeps the product response consistent with the // attribution validation performed when an order is created. func filterOtherVariantProducts( response *proto.ProductRes, currentProductIDs, otherProductIDs []primitive.ObjectID, ) { if response == nil { return } current := make(map[primitive.ObjectID]struct{}, len(currentProductIDs)) for _, productID := range currentProductIDs { current[productID] = struct{}{} } blocked := make(map[primitive.ObjectID]struct{}, len(otherProductIDs)) for _, productID := range otherProductIDs { if _, shared := current[productID]; !shared { blocked[productID] = struct{}{} } } if len(blocked) == 0 { return } positions := make([]proto.ProductList, 0, len(response.List)) for _, position := range response.List { products := make([]proto.VIPListRes, 0, len(position.List)) for _, product := range position.List { if _, excluded := blocked[product.ID]; excluded { continue } products = append(products, product) } if len(products) == 0 { continue } position.List = products positions = append(positions, position) } response.List = positions } func RecordEvents(uid uint64, request EventsRequest, now time.Time) (EventsResponse, error) { response := EventsResponse{} if uid == 0 { return response, fmt.Errorf("authenticated user is required") } if len(request.Events) == 0 || len(request.Events) > maxEventsPerRequest { return response, fmt.Errorf("events must contain 1-%d items", maxEventsPerRequest) } experiments := make(map[string]*vipcardexperimentmod.Experiment) events := make([]*vipcardexperimentmod.AnalyticsEvent, 0, len(request.Events)) for _, input := range request.Events { event, experiment, err := validateEvent(uid, input, now, experiments) if err != nil { return EventsResponse{}, err } events = append(events, event) experiments[experiment.ExperimentID] = experiment } for _, event := range events { inserted, err := vipcardexperimentmod.InsertEvent(event) if err != nil { return EventsResponse{}, err } if inserted { response.Accepted++ } else { response.Duplicated++ } } return response, nil } func validateEvent( uid uint64, input EventInput, now time.Time, experiments map[string]*vipcardexperimentmod.Experiment, ) (*vipcardexperimentmod.AnalyticsEvent, *vipcardexperimentmod.Experiment, error) { input.EventID = strings.TrimSpace(input.EventID) input.EventName = strings.ToUpper(strings.TrimSpace(input.EventName)) input.SessionID = strings.TrimSpace(input.SessionID) input.ExperimentID = strings.TrimSpace(input.ExperimentID) input.Variant = strings.ToUpper(strings.TrimSpace(input.Variant)) if input.EventID == "" || len(input.EventID) > 128 { return nil, nil, fmt.Errorf("eventId is required and must not exceed 128 characters") } if !vipcardexperimentmod.ValidEventName(input.EventName) { return nil, nil, fmt.Errorf("unsupported eventName: %s", input.EventName) } if input.SessionID == "" || len(input.SessionID) > 128 { return nil, nil, fmt.Errorf("sessionId is required and must not exceed 128 characters") } if input.ExperimentID == "" { return nil, nil, fmt.Errorf("experimentId is required") } if len(input.ExperimentID) > 128 { return nil, nil, fmt.Errorf("experimentId must not exceed 128 characters") } if input.OccurredAt.IsZero() || input.OccurredAt.After(now.Add(5*time.Minute)) { return nil, nil, fmt.Errorf("occurredAt is invalid") } experiment := experiments[input.ExperimentID] var err error if experiment == nil { experiment, err = vipcardexperimentmod.FindByExperimentID(input.ExperimentID) if err != nil { return nil, nil, err } if experiment == nil { return nil, nil, fmt.Errorf("experiment does not exist") } } if !eventOccurredWithinExperiment(experiment, input.OccurredAt) { return nil, nil, fmt.Errorf("occurredAt is outside the experiment period") } config, ok := experiment.ConfigFor(input.Variant) if !ok { return nil, nil, fmt.Errorf("variant must be A or B") } if assigned := experiment.Assign(uid); assigned != input.Variant { return nil, nil, fmt.Errorf("variant does not match user assignment") } if input.EventName == vipcardexperimentmod.EventProductImpression { if input.ProductID.IsZero() { return nil, nil, fmt.Errorf("productId is required for VIP_PRODUCT_IMPRESSION") } if !containsProduct(config.ProductIDs, input.ProductID) { return nil, nil, fmt.Errorf("productId does not belong to variant") } } else { input.ProductID = primitive.NilObjectID } return &vipcardexperimentmod.AnalyticsEvent{ EventID: input.EventID, EventName: input.EventName, UID: uid, SessionID: input.SessionID, OccurredAt: input.OccurredAt, ExperimentID: input.ExperimentID, Variant: input.Variant, ProductID: input.ProductID, }, experiment, nil } func eventOccurredWithinExperiment(experiment *vipcardexperimentmod.Experiment, occurredAt time.Time) bool { const clockSkew = 5 * time.Minute startAt := experiment.PublishedAt if experiment.StartAt != nil && (startAt.IsZero() || experiment.StartAt.After(startAt)) { startAt = *experiment.StartAt } if !startAt.IsZero() && occurredAt.Before(startAt.Add(-clockSkew)) { return false } var endAt *time.Time if experiment.EndAt != nil { end := *experiment.EndAt endAt = &end } if experiment.DisabledAt != nil && (endAt == nil || experiment.DisabledAt.Before(*endAt)) { end := *experiment.DisabledAt endAt = &end } return endAt == nil || !occurredAt.After(endAt.Add(clockSkew)) } func containsProduct(ids []primitive.ObjectID, target primitive.ObjectID) bool { for _, id := range ids { if id == target { return true } } return false }