Files
hi-server/internal/model/lottery/registry.go
T
shanshanzhong147 9933d34bdd 新功能(#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 + 集成/并发/概率测试。
2026-07-08 20:05:05 -07:00

95 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }