新功能(#3): 抽奖 Stage 1 handler 真实业务对接 + 邀请钩子
Closes HIF-3 (阶段 PR B) PR B:handler 真实业务对接 + 邀请钩子(迭代含 R1 修复) - 迁移 02157_lottery_grant_ledger:external_ref UNIQUE 作为发奖幂等键 - log.CommissionTypeLottery=339(架构师批准的新常量) - DispatchRequest.IdempotencyKey(架构师 review 建议第 2 条) - GrantLedger + LedgerService.Reserve:INSERT ON CONFLICT DO NOTHING 幂等 upsert - VPNDurationHandler:ResolveEffectiveUser 归位家庭 owner + UpdateSubscribe,ExpireTime 三分支对齐 grantGiftDays - CommissionHandler:UpdateCommission + WriteCommissionLog(339),发给中奖者本人,不做家庭组归位 - Handler 从 model 层迁到 logic 层(避免 model → logic 反向依赖);noop 保留在 model 层 - InviteHook:fire-and-forget 独立 goroutine + 10s timeout,扫 running 活动的 invite_success 源 - ServiceContext 新增 LotteryChance / LotteryLedger / LotteryInviteHook - activateOrderLogic.handleCommission 两条分支通过 invokeInviteHookIfEligible 助手触发,助手内统一 gate IsNew(架构师 R1 打回后的修复) 架构师 R1 打回:branch B 未按"首次付款激活"gate,续费也会给 referer 发抽奖机会 → 已通过助手函数集中收拢,避免 branch A/B 判断漂移。 测试覆盖率:model 76.5% / handler 73.2% / hook 91.7%;补 4 个回归测试覆盖 IsNew 门槛(含关键的 DoesNotFireOnRenewal)。 线上仍零可见变更:无对外路由,钩子仅在活动 status=running 时生效,Stage 1 全流程无活动记录时 loadRunningActivities 返回空。
This commit is contained in:
@@ -1127,6 +1127,11 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
// 普通用户路径(佣金比例=0):只有首单才双方赠N天
|
||||
if orderInfo.IsNew {
|
||||
l.grantGiftDaysToBothParties(ctx, userInfo, orderInfo)
|
||||
// 抽奖钩子(Stage 1):首单成功即算邀请转化,即便本笔无佣金。
|
||||
// referer 通过 userInfo.RefererId 定位,orderNo 作幂等键。
|
||||
if userInfo != nil {
|
||||
l.invokeInviteHookIfEligible(ctx, userInfo.RefererId, orderInfo)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1214,6 +1219,11 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
)
|
||||
}
|
||||
|
||||
// 抽奖钩子(Stage 1):仅新购激活时触发(IsNew)。续费走留存路径不再发放
|
||||
// 机会,避免注册后弃号刷奖的黑产。branch B 内不再显式判 IsNew,全部通过
|
||||
// invokeInviteHookIfEligible 收拢,语义唯一入口。
|
||||
l.invokeInviteHookIfEligible(ctx, referer.Id, orderInfo)
|
||||
|
||||
// 有佣金路径:邀请人拿佣金,被邀请用户(首单)拿天数
|
||||
if orderInfo.IsNew {
|
||||
giftTarget := l.resolveGiftTargetUser(ctx, userInfo, orderInfo.SubscriptionUserId)
|
||||
@@ -1223,6 +1233,22 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
}
|
||||
}
|
||||
|
||||
// invokeInviteHookIfEligible 是抽奖邀请钩子的唯一入口。所有 handleCommission
|
||||
// 分支都通过它触发,避免"这里加了 IsNew 判断,那里忘了"的漂移。
|
||||
//
|
||||
// 门槛:仅在 orderInfo.IsNew=true(新购首次激活)时触发 —— 这是架构师锁定
|
||||
// 的"首次付款激活"决策。续费是留存事件而非转化事件,不发放抽奖机会(否则
|
||||
// 会催生注册后弃号刷奖的黑产)。
|
||||
func (l *ActivateOrderLogic) invokeInviteHookIfEligible(ctx context.Context, refererID int64, orderInfo *order.Order) {
|
||||
if l.svc == nil || l.svc.LotteryInviteHook == nil || orderInfo == nil {
|
||||
return
|
||||
}
|
||||
if !orderInfo.IsNew {
|
||||
return
|
||||
}
|
||||
l.svc.LotteryInviteHook.OnConversion(ctx, refererID, orderInfo.OrderNo)
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderInfo *order.Order) {
|
||||
giftDays := l.svc.Config.Invite.GiftDays
|
||||
if giftDays <= 0 || referee == nil || referee.Id == 0 || referee.RefererId == 0 || orderInfo == nil {
|
||||
|
||||
Reference in New Issue
Block a user