修复(#128): 按规则类型决定促销资格,让 InactiveUser/Campaign 对老用户生效
Build docker and publish / build (20.15.1) (push) Has been cancelled
Build docker and publish / build (20.15.1) (pull_request) Has been cancelled

此前 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:
2026-05-31 19:00:30 -07:00
parent ae126296e3
commit 7236ca4cf2
7 changed files with 195 additions and 20 deletions
+13 -3
View File
@@ -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, &currentUser, now)
eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, isFirstPurchase, &currentUser, 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)