0a897419a5
Co-authored-by: multica-agent <github@multica.ai>
189 lines
4.7 KiB
Go
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), ¶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(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{}
|
|
}
|