修复(#128): 按规则类型决定促销资格,让 InactiveUser/Campaign 对老用户生效
此前 calculatePurchasePrice 把 allowPromo 绑死在 orderType == 1,导致只要用户
有任何过往付费订阅(含已过期),就会被 paidSubscriptionQuery 路由为续费
(orderType=2),跳过所有促销评估。后果:
- InactiveUser 召回促销永远无法触发(其目标人群恰好就是有过期订阅的用户)
- Campaign 全员活动对老用户 / 升级加购场景完全失效
- 套餐列表(loadSubscribePromoMap)直接调 EvaluatePromo 不感知 orderType,
可能显示促销价但下单时却拿到原价
修复方式:把 isFirstPurchase 下放给 EvaluatePromo,由规则类型决定 gating:
- NewUser 要求 isFirstPurchase=true(保留首购语义)
- InactiveUser 由规则自身的"上次订阅过期 N 月以上"条件判定
- Campaign 时间窗内对任意用户生效
新增 common.HasPaidSubscription 助手,套餐列表与下单走同一份 isFirstPurchase
判定,确保展示价与实际下单价口径一致。
测试:补充 EvaluatePromo / calculatePurchasePrice 的 NewUser 屏蔽 + Campaign
放开用例;更新 loadSubscribePromoMap 测试覆盖新增 HasPaidSubscription 查询。
注:renewalLogic.go 仍未接入促销(属于方向 B 的彻底统一,本次未涵盖)。
This commit is contained in:
@@ -28,7 +28,16 @@ type promoRuleParams struct {
|
||||
InactiveMonths int `json:"inactive_months"`
|
||||
}
|
||||
|
||||
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64, quantity int64) (*PromoResult, error) {
|
||||
// EvaluatePromo decides whether the given (user, subscribe, quantity) tuple
|
||||
// qualifies for any active promo rule. Each rule type has its own gating:
|
||||
// - new_user: requires isFirstPurchase=true (no prior paid subscription)
|
||||
// - inactive_user: requires a previously expired subscription (rule self-check)
|
||||
// - campaign: applies unconditionally within the configured time window
|
||||
//
|
||||
// Pass isFirstPurchase=true on order paths where the request is being routed
|
||||
// as a brand-new purchase; pass false when the user already has a paid
|
||||
// subscription (including expired ones) so NewUser cannot be reused.
|
||||
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64, quantity int64, isFirstPurchase bool) (*PromoResult, error) {
|
||||
result := &PromoResult{}
|
||||
if svcCtx == nil || svcCtx.PromoModel == nil || svcCtx.DB == nil || subscribeID <= 0 || quantity <= 0 {
|
||||
return result, nil
|
||||
@@ -59,7 +68,7 @@ func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64
|
||||
}
|
||||
}
|
||||
|
||||
eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, ¤tUser, now)
|
||||
eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, isFirstPurchase, ¤tUser, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,12 +105,13 @@ func evaluatePromoRule(
|
||||
rule *promo.RuleWithPrice,
|
||||
params promoRuleParams,
|
||||
userID int64,
|
||||
isFirstPurchase bool,
|
||||
currentUser *user.User,
|
||||
now time.Time,
|
||||
) (bool, time.Time, error) {
|
||||
switch rule.Type {
|
||||
case promo.RuleTypeNewUser:
|
||||
if userID <= 0 {
|
||||
if userID <= 0 || !isFirstPurchase {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
return evaluateNewUserPromo(ctx, db, params, userID, currentUser, now)
|
||||
|
||||
Reference in New Issue
Block a user