package common import ( "context" "encoding/json" "time" "github.com/perfect-panel/server/internal/model/promo" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) type PromoResult struct { Eligible bool RuleID int64 RuleName string RuleType string PromoPrice int64 ExpiresAt time.Time } type promoRuleParams struct { WindowHours int `json:"window_hours"` InactiveMonths int `json:"inactive_months"` } func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64) (*PromoResult, error) { result := &PromoResult{} if svcCtx == nil || svcCtx.PromoModel == nil || svcCtx.DB == nil || userID <= 0 || subscribeID <= 0 { return result, nil } rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo rules failed: %v", err.Error()) } if len(rules) == 0 { return result, nil } var currentUser user.User now := time.Now() for _, rule := range rules { if rule == nil || !isPromoRuleInTimeWindow(rule, now) { continue } if rule.PromoPrice <= 0 { continue } params := promoRuleParams{} if rule.Params != "" { if err = json.Unmarshal([]byte(rule.Params), ¶ms); err != nil { continue } } eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, ¤tUser, now) if err != nil { return nil, err } if !eligible { continue } return &PromoResult{ Eligible: true, RuleID: rule.Id, RuleName: rule.Name, RuleType: rule.Type, PromoPrice: rule.PromoPrice, ExpiresAt: expiresAt, }, nil } return result, nil } func isPromoRuleInTimeWindow(rule *promo.RuleWithPrice, now time.Time) bool { if rule.StartTime != nil && !rule.StartTime.IsZero() && now.Before(*rule.StartTime) { return false } if rule.EndTime != nil && !rule.EndTime.IsZero() && now.After(*rule.EndTime) { return false } return true } func evaluatePromoRule( ctx context.Context, db *gorm.DB, rule *promo.RuleWithPrice, params promoRuleParams, userID int64, currentUser *user.User, now time.Time, ) (bool, time.Time, error) { switch rule.Type { case promo.RuleTypeNewUser: return evaluateNewUserPromo(ctx, db, params, userID, currentUser, now) case promo.RuleTypeInactiveUser: return evaluateInactiveUserPromo(ctx, db, params, userID, promoRuleExpiresAt(rule), now) case promo.RuleTypeCampaign: return true, promoRuleExpiresAt(rule), nil default: return false, time.Time{}, nil } } func evaluateNewUserPromo( ctx context.Context, db *gorm.DB, params promoRuleParams, userID int64, currentUser *user.User, now time.Time, ) (bool, time.Time, error) { if params.WindowHours <= 0 { return false, time.Time{}, nil } if currentUser.Id == 0 { if err := db.WithContext(ctx).Model(&user.User{}).Where("id = ?", userID).First(currentUser).Error; err != nil { return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user failed") } } expiresAt := currentUser.CreatedAt.Add(time.Duration(params.WindowHours) * time.Hour) return now.Before(expiresAt), expiresAt, nil } func evaluateInactiveUserPromo( ctx context.Context, db *gorm.DB, params promoRuleParams, userID int64, ruleExpiresAt time.Time, now time.Time, ) (bool, time.Time, error) { if params.InactiveMonths <= 0 { return false, time.Time{}, nil } var lastSub user.Subscribe err := db.WithContext(ctx). Model(&user.Subscribe{}). Where("user_id = ?", userID). Order("expire_time DESC"). Limit(1). Take(&lastSub).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return true, ruleExpiresAt, nil } return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed") } threshold := now.AddDate(0, -params.InactiveMonths, 0) return lastSub.ExpireTime.Before(threshold) || lastSub.ExpireTime.Equal(threshold), ruleExpiresAt, nil } func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time { if rule != nil && rule.EndTime != nil { return *rule.EndTime } return time.Time{} }