Files
hi-server/internal/model/lottery/registry.go
T
shanshanzhong147 07409eb602 新功能(#4): 抽奖 Stage 2 人工奖领奖工单(crypto / physical / manual_other)
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%
2026-07-10 04:01:34 -07:00

58 lines
1.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"
"encoding/json"
"sync"
"gorm.io/gorm"
)
// defaultRegistry 是并发安全的默认 Registry。启动时一次性注册,运行时只读。
type defaultRegistry struct {
mu sync.RWMutex
handlers map[string]PrizeHandler
}
// NewRegistry 返回空的默认注册表。使用 Register 挂接实现。
// 实际业务 handlervpn_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 }