新功能(#3): 抽奖活动 Stage 1 骨架(DB + 规则引擎 + Handler 抽象)
Closes HIF-3 Stage 1 骨架:7 张表迁移 + 门槛规则引擎 + 加权选奖 + 次数入账/消耗(幂等)+ 发奖 handler 抽象与注册表。 - 单测覆盖 75.5%(未覆盖行 = stub handler ErrNotImplemented,合理) - CI 全绿(构建/Vet/测试 + golangci-lint) - 骨架不接入真实业务,vpn_duration/commission handler 在 Dispatch 中返回 ErrNotImplemented;合并后线上零变更 架构师 review 通过,4 项决策已在 issue 上给出: 1. 邀请转化语义 = 首次付款激活 2. 佣金日志类型 = 新增 CommissionTypeLottery=339 3. 家庭组归属 = 穿透到 owner 4. 管理端 IP 白名单 = 不做(推到 nginx/ingress 层) 后续 PR B/C 补真实业务对接 + 用户 API + 后台 CRUD + 集成/并发/概率测试。
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package lottery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// defaultRegistry 是并发安全的默认 Registry。启动时一次性注册,运行时只读。
|
||||
type defaultRegistry struct {
|
||||
mu sync.RWMutex
|
||||
handlers map[string]PrizeHandler
|
||||
}
|
||||
|
||||
// NewRegistry 返回空的默认注册表。使用 Register 挂接实现。
|
||||
func NewRegistry() *defaultRegistry {
|
||||
return &defaultRegistry{handlers: make(map[string]PrizeHandler, 8)}
|
||||
}
|
||||
|
||||
// Register 挂接一个 handler;重复注册会覆盖(初始化阶段可控,不额外拦截)。
|
||||
func (r *defaultRegistry) Register(h PrizeHandler) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.handlers[h.Type()] = h
|
||||
}
|
||||
|
||||
func (r *defaultRegistry) Get(prizeType string) (PrizeHandler, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
h, ok := r.handlers[prizeType]
|
||||
return h, ok
|
||||
}
|
||||
|
||||
func (r *defaultRegistry) MustGet(prizeType string) (PrizeHandler, error) {
|
||||
if h, ok := r.Get(prizeType); ok {
|
||||
return h, nil
|
||||
}
|
||||
return nil, ErrHandlerNotRegistered
|
||||
}
|
||||
|
||||
// ---- Stage 1 骨架 handler --------------------------------------------------
|
||||
// 真实业务对接在架构师 review 骨架 PR 之后 Day 2-3 补齐。此时命中真实
|
||||
// handler 会返回 ErrNotImplemented,让抽奖流程整笔事务回滚,避免误发。
|
||||
|
||||
// noopHandler 是"谢谢参与"。写记录、不发放,不需要真实业务对接。
|
||||
type noopHandler struct{}
|
||||
|
||||
// NewNoopHandler 返回 PrizeTypeNone 的 handler。
|
||||
func NewNoopHandler() PrizeHandler { return &noopHandler{} }
|
||||
|
||||
func (noopHandler) Type() string { return PrizeTypeNone }
|
||||
func (noopHandler) IsAuto() bool { return true }
|
||||
func (noopHandler) Dispatch(_ context.Context, _ *gorm.DB, _ DispatchRequest) (DispatchResult, error) {
|
||||
return DispatchResult{State: DispatchStateAutoClaimed, Message: "谢谢参与"}, nil
|
||||
}
|
||||
func (noopHandler) ValidateClaim(_ []byte) error { return nil }
|
||||
|
||||
// vpnDurationStubHandler 是订阅时长发放的占位 handler。
|
||||
// Day 2-3 需替换为真实实现:
|
||||
// 1. tx 内 SELECT ... FOR UPDATE 锁 lottery_grant_ledger UNIQUE(external_ref)
|
||||
// 2. 命中 → 幂等返回;未命中 → INSERT ledger + 调用 UserModel.UpdateSubscribe/InsertSubscribe
|
||||
// 3. external_ref 用 fmt.Sprintf("lottery:%d:%d", req.ActivityId, req.DrawId)
|
||||
// 4. 家庭组场景通过 commonLogic.ResolveEntitlementUser 归位到 owner
|
||||
// 5. 成功后 best-effort 调 NodeModel.ClearServerAllCache + 用户组重算
|
||||
type vpnDurationStubHandler struct{}
|
||||
|
||||
// NewVPNDurationStubHandler 返回 PrizeTypeVPNDuration 的骨架 handler。
|
||||
func NewVPNDurationStubHandler() PrizeHandler { return &vpnDurationStubHandler{} }
|
||||
|
||||
func (vpnDurationStubHandler) Type() string { return PrizeTypeVPNDuration }
|
||||
func (vpnDurationStubHandler) IsAuto() bool { return true }
|
||||
func (vpnDurationStubHandler) Dispatch(_ context.Context, _ *gorm.DB, _ DispatchRequest) (DispatchResult, error) {
|
||||
return DispatchResult{}, ErrNotImplemented
|
||||
}
|
||||
func (vpnDurationStubHandler) ValidateClaim(_ []byte) error { return nil }
|
||||
|
||||
// commissionStubHandler 是佣金入账的占位 handler。
|
||||
// Day 2-3 需替换为真实实现:
|
||||
// 1. tx 内 lottery_grant_ledger 幂等检查
|
||||
// 2. 调 UserModel.UpdateCommission + common.WriteCommissionLog(同 tx)
|
||||
// 3. 建议新增 log.CommissionTypeLottery 常量(数值待架构师批),
|
||||
// 避免把抽奖入账混进 Purchase/Renewal 类型统计
|
||||
type commissionStubHandler struct{}
|
||||
|
||||
// NewCommissionStubHandler 返回 PrizeTypeCommission 的骨架 handler。
|
||||
func NewCommissionStubHandler() PrizeHandler { return &commissionStubHandler{} }
|
||||
|
||||
func (commissionStubHandler) Type() string { return PrizeTypeCommission }
|
||||
func (commissionStubHandler) IsAuto() bool { return true }
|
||||
func (commissionStubHandler) Dispatch(_ context.Context, _ *gorm.DB, _ DispatchRequest) (DispatchResult, error) {
|
||||
return DispatchResult{}, ErrNotImplemented
|
||||
}
|
||||
func (commissionStubHandler) ValidateClaim(_ []byte) error { return nil }
|
||||
Reference in New Issue
Block a user