新功能(#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:
@@ -0,0 +1,71 @@
|
||||
package lottery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// GrantLedger 是发奖账本一行。UNIQUE(external_ref) 是幂等键的载体:
|
||||
// 每次 PrizeHandler.Dispatch 用 DispatchRequest.IdempotencyKey 作 external_ref,
|
||||
// INSERT 冲突即"已发过",直接返回持久化的原结果。
|
||||
type GrantLedger struct {
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
ExternalRef string `gorm:"type:varchar(128);not null;uniqueIndex:uk_external_ref;comment:幂等键"`
|
||||
HandlerType string `gorm:"type:varchar(32);not null;comment:handler 类型"`
|
||||
UserId int64 `gorm:"type:bigint unsigned;not null;comment:发放对象用户 ID"`
|
||||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:活动 ID"`
|
||||
DrawId int64 `gorm:"type:bigint unsigned;not null;comment:抽奖记录 ID"`
|
||||
Amount int64 `gorm:"type:bigint;not null;default:0;comment:发放数量"`
|
||||
Payload string `gorm:"type:json;comment:发放后的关键结果快照"`
|
||||
GrantedAt time.Time `gorm:"<-:create;default:CURRENT_TIMESTAMP;comment:发放完成时间"`
|
||||
}
|
||||
|
||||
// TableName 对齐 02157 migration。
|
||||
func (GrantLedger) TableName() string { return "lottery_grant_ledger" }
|
||||
|
||||
// LedgerService 处理发奖账本的幂等 upsert。所有 handler 的第一步都是它。
|
||||
type LedgerService interface {
|
||||
// Reserve 尝试为 external_ref 抢占一行账本。
|
||||
// - 未冲突 → 返回新建行,caller 继续调用下游业务;提交事务时账本一起落。
|
||||
// - 冲突 → 返回已存在的账本行,caller 视为幂等命中直接返回。
|
||||
// 传入 tx 必须是 caller 的事务句柄,保证账本行随抽奖事务一起提交。
|
||||
Reserve(ctx context.Context, tx *gorm.DB, entry GrantLedger) (row *GrantLedger, alreadyExisted bool, err error)
|
||||
}
|
||||
|
||||
type ledgerService struct{}
|
||||
|
||||
// NewLedgerService 返回默认账本服务。
|
||||
func NewLedgerService() LedgerService { return &ledgerService{} }
|
||||
|
||||
// Reserve 用 INSERT ... ON CONFLICT DO NOTHING 抢占 external_ref。
|
||||
// 未命中时再走一次 SELECT 拿到实际持久化的行(不管是新插的还是旧的),
|
||||
// 目的是让 caller 拿到统一的 GrantLedger 结构,方便回写 draw 状态。
|
||||
func (s *ledgerService) Reserve(ctx context.Context, tx *gorm.DB, entry GrantLedger) (*GrantLedger, bool, error) {
|
||||
if tx == nil {
|
||||
return nil, false, errors.New("Reserve requires a transaction handle")
|
||||
}
|
||||
if entry.ExternalRef == "" {
|
||||
return nil, false, errors.New("Reserve requires a non-empty ExternalRef")
|
||||
}
|
||||
|
||||
insertRes := tx.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&entry)
|
||||
if insertRes.Error != nil {
|
||||
return nil, false, insertRes.Error
|
||||
}
|
||||
alreadyExisted := insertRes.RowsAffected == 0
|
||||
|
||||
// 读回持久化的行,避免依赖 gorm 的 AutoIncrement 回填在冲突分支不确定的行为。
|
||||
var stored GrantLedger
|
||||
if err := tx.WithContext(ctx).
|
||||
Where("external_ref = ?", entry.ExternalRef).
|
||||
First(&stored).Error; err != nil {
|
||||
return nil, alreadyExisted, err
|
||||
}
|
||||
return &stored, alreadyExisted, nil
|
||||
}
|
||||
Reference in New Issue
Block a user