a46fb83054
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 返回空。
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
package orderLogic
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
lotteryhook "github.com/perfect-panel/server/internal/logic/lottery/hook"
|
|
"github.com/perfect-panel/server/internal/model/order"
|
|
"github.com/perfect-panel/server/internal/svc"
|
|
)
|
|
|
|
// recordingInviteHook 记录每次 OnConversion 调用,让测试直接断言时序与参数。
|
|
type recordingInviteHook struct {
|
|
mu sync.Mutex
|
|
calls []recordingInviteCall
|
|
}
|
|
|
|
type recordingInviteCall struct {
|
|
refererID int64
|
|
orderNo string
|
|
}
|
|
|
|
func (h *recordingInviteHook) OnConversion(_ context.Context, refererID int64, orderNo string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.calls = append(h.calls, recordingInviteCall{refererID: refererID, orderNo: orderNo})
|
|
}
|
|
|
|
// verify recordingInviteHook satisfies the interface at compile time.
|
|
var _ lotteryhook.InviteHook = (*recordingInviteHook)(nil)
|
|
|
|
func (h *recordingInviteHook) snapshot() []recordingInviteCall {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
out := make([]recordingInviteCall, len(h.calls))
|
|
copy(out, h.calls)
|
|
return out
|
|
}
|
|
|
|
func newLotteryHookTestLogic(hook lotteryhook.InviteHook) *ActivateOrderLogic {
|
|
return NewActivateOrderLogic(&svc.ServiceContext{LotteryInviteHook: hook})
|
|
}
|
|
|
|
// TestInvokeInviteHook_FiresOnFirstPurchase 保证新购激活会触发抽奖钩子。
|
|
// Positive-path regression matched against the "首次付款激活" decision.
|
|
func TestInvokeInviteHook_FiresOnFirstPurchase(t *testing.T) {
|
|
hook := &recordingInviteHook{}
|
|
logic := newLotteryHookTestLogic(hook)
|
|
|
|
logic.invokeInviteHookIfEligible(context.Background(), 42, &order.Order{
|
|
OrderNo: "ORD-NEW-1",
|
|
IsNew: true,
|
|
})
|
|
|
|
calls := hook.snapshot()
|
|
if len(calls) != 1 {
|
|
t.Fatalf("expected exactly 1 OnConversion call on first purchase, got %+v", calls)
|
|
}
|
|
if calls[0].refererID != 42 || calls[0].orderNo != "ORD-NEW-1" {
|
|
t.Fatalf("wrong args: %+v", calls[0])
|
|
}
|
|
}
|
|
|
|
// TestInvokeInviteHook_DoesNotFireOnRenewal 是架构师 R1 打回的关键回归:
|
|
// 续费付款必须 NOT 触发抽奖钩子,否则 referer 每月拿一次白嫖机会,与"首次
|
|
// 付款激活"决策相悖。
|
|
func TestInvokeInviteHook_DoesNotFireOnRenewal(t *testing.T) {
|
|
hook := &recordingInviteHook{}
|
|
logic := newLotteryHookTestLogic(hook)
|
|
|
|
logic.invokeInviteHookIfEligible(context.Background(), 42, &order.Order{
|
|
OrderNo: "ORD-RENEWAL-1",
|
|
IsNew: false,
|
|
})
|
|
|
|
if calls := hook.snapshot(); len(calls) != 0 {
|
|
t.Fatalf("renewal must NOT trigger invite hook; got %+v", calls)
|
|
}
|
|
}
|
|
|
|
// TestInvokeInviteHook_NilOrderIsSafe 保证空 order 输入不 panic(防御性)。
|
|
func TestInvokeInviteHook_NilOrderIsSafe(t *testing.T) {
|
|
hook := &recordingInviteHook{}
|
|
logic := newLotteryHookTestLogic(hook)
|
|
logic.invokeInviteHookIfEligible(context.Background(), 42, nil)
|
|
if calls := hook.snapshot(); len(calls) != 0 {
|
|
t.Fatalf("nil order must be a no-op; got %+v", calls)
|
|
}
|
|
}
|
|
|
|
// TestInvokeInviteHook_NilHookIsSafe 保证 ServiceContext 里没接线时不 panic。
|
|
func TestInvokeInviteHook_NilHookIsSafe(t *testing.T) {
|
|
logic := NewActivateOrderLogic(&svc.ServiceContext{}) // no LotteryInviteHook
|
|
logic.invokeInviteHookIfEligible(context.Background(), 42, &order.Order{
|
|
OrderNo: "ORD-1",
|
|
IsNew: true,
|
|
})
|
|
// If we got here without panicking, we pass.
|
|
}
|