07409eb602
Closes HIF-4 Stage 2 交付:人工奖领奖工单完整闭环。crypto / physical / manual_other 三类奖品从抽中到 mark-paid 的全流程可用。 - 迁移 02159_lottery_claim:UNIQUE(draw_id) + 3 支持索引,状态机 pending_claim→reviewing→paying→paid,rejected 可复活,超时 expired - 3 个 PrizeHandler:Dispatch→ErrDispatchNotSupported 兜底、ClaimSchema 各自形态、ValidateClaim 表驱动 - BuildCryptoClaimSchema:抽中时按奖品 config.networks 注入 enum,前端下拉直接可用 - Draw service dispatchOrEnqueueClaim:人工奖同 tx 插 pending_claim(回滚双清),nonce 重放回读 ExpiresAt + ClaimFormSchema - POST /claim 实装:ownership 校验 → prize 类型校验 → handler.ValidateClaim → crypto network 白名单二次校验 → tx CAS status IN (pending_claim, rejected) AND expires_at > now - Admin CRUD 5 接口:list(IN 批拉 snap + user,无 N+1)、summary(GROUP BY 一次拿计数 + overdue 单查)、approve/reject/mark-paid 全走 CAS + audit - Scheduler @every 1h 扫过期,级联 lottery_draw.dispatch_state → expired - 新增错误码 100005-100011(already_submitted / invalid_claim_data / draw_not_found / not_your_draw / claim_expired / claim_state_invalid) - Rebase 后 Stage 2 测试主动 reuse PR E 的 unmetReasonsNotEmpty + evaluatedAtNotZero matcher,人工奖分支若绕过守卫会立即挂 - Stage 1 全部 4 处 guardrail 后端 rebase 时自检过:UnmetReasons、EvaluatedAt、GrantLedger.Payload、AdminMetaMiddleware 全保留 CI 全绿;28 files, +2442/-126;覆盖率 handler 78.9% / model.lottery 74.3% / draw 68.7% / queue/lottery 76.9%
58 lines
1.8 KiB
Go
58 lines
1.8 KiB
Go
package lottery
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"sync"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// defaultRegistry 是并发安全的默认 Registry。启动时一次性注册,运行时只读。
|
||
type defaultRegistry struct {
|
||
mu sync.RWMutex
|
||
handlers map[string]PrizeHandler
|
||
}
|
||
|
||
// NewRegistry 返回空的默认注册表。使用 Register 挂接实现。
|
||
// 实际业务 handler(vpn_duration / commission)在 internal/logic/lottery/handler 里,
|
||
// 由 initialize 阶段注入(避免 model 层反向依赖 logic 层)。
|
||
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
|
||
}
|
||
|
||
// noopHandler 是"谢谢参与",永远归 model 层:不依赖任何业务,最小占位。
|
||
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 }
|
||
func (noopHandler) ClaimSchema() json.RawMessage { return nil }
|