Files
hi-server/internal/logic/common/promoEligibility.go
T
shanshanzhong147 d2f9289338
Build docker and publish / build (20.15.1) (push) Has been cancelled
Build docker and publish / build (20.15.1) (pull_request) Successful in 8m28s
新功能(#76): 促销系统下单流程集成
- 新增促销规则/规格促销价/使用记录模型与幂等迁移
- EvaluatePromo 支持 new_user/inactive_user/campaign 规则判定
- purchase/preCreate 接入促销判定:命中促销时跳过百分比折扣
- activate 激活后按 order_no 幂等写入 promo_usage
- 订单模型和响应结构新增 promo_rule_id、promo_discount 字段

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 22:18:44 -07:00

171 lines
4.2 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"
)
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("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{}
}