Files
hi-server/internal/logic/common/promoEligibility.go
T
2026-05-27 01:44:11 -07:00

189 lines
4.7 KiB
Go

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"
"gorm.io/gorm/clause"
)
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), &params); err != nil {
continue
}
}
eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, &currentUser, 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(clause.OrderBy{
Expression: clause.Expr{
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END",
Vars: []interface{}{permanentSubscribeExpireTime()},
},
}).
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")
}
return isInactivePromoEligible(lastSub.ExpireTime, now, params.InactiveMonths), ruleExpiresAt, nil
}
func isInactivePromoEligible(expireTime time.Time, now time.Time, inactiveMonths int) bool {
if expireTime.Equal(permanentSubscribeExpireTime()) {
return false
}
threshold := now.AddDate(0, -inactiveMonths, 0)
return expireTime.Before(threshold) || expireTime.Equal(threshold)
}
func permanentSubscribeExpireTime() time.Time {
return time.UnixMilli(0)
}
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
if rule != nil && rule.EndTime != nil {
return *rule.EndTime
}
return time.Time{}
}