修复(#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
+8 -1
View File
@@ -42,12 +42,19 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
userInfo, _ := ctx.Value(constant.CtxKeyUser).(*user.User)
userID := int64(0)
isFirstPurchase := true
if userInfo != nil {
entitlement, err := commonLogic.ResolveEntitlementUser(ctx, svcCtx.DB, userInfo.Id)
if err != nil {
return nil, err
}
userID = entitlement.EffectiveUserID
hasPaid, err := commonLogic.HasPaidSubscription(ctx, svcCtx.DB, userID)
if err != nil {
return nil, err
}
isFirstPurchase = !hasPaid
}
candidates, err := querySubscribePromoCandidates(ctx, svcCtx, subscribeIDs, userInfo != nil)
@@ -68,7 +75,7 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
continue
}
promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, candidate.SubscribeId, candidate.Quantity)
promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, candidate.SubscribeId, candidate.Quantity, isFirstPurchase)
if err != nil {
return nil, err
}
@@ -105,6 +105,9 @@ func TestLoadSubscribePromoMapUsesFamilyOwnerForInactivePromo(t *testing.T) {
WithArgs(memberUserID, user.FamilyMemberActive, 1).
WillReturnRows(sqlmock.NewRows([]string{"role", "family_status", "owner_user_id"}).
AddRow(user.FamilyRoleMember, user.FamilyStatusActive, ownerUserID))
mock.ExpectQuery("SELECT count(*) FROM `user_subscribe`").
WithArgs(ownerUserID, 1).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
mock.ExpectQuery("FROM subscribe_promo AS sp").
WillReturnRows(sqlmock.NewRows([]string{
"subscribe_id", "quantity", "rule_name", "rule_type", "promo_price", "params", "start_time", "end_time",