9933d34bdd
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 + 集成/并发/概率测试。
185 lines
8.7 KiB
Go
185 lines
8.7 KiB
Go
package lottery
|
||
|
||
import (
|
||
"database/sql"
|
||
"encoding/json"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ---- 状态与类型常量 ---------------------------------------------------------
|
||
|
||
const (
|
||
ActivityStatusDraft = "draft"
|
||
ActivityStatusRunning = "running"
|
||
ActivityStatusPaused = "paused"
|
||
ActivityStatusEnded = "ended"
|
||
|
||
UnmetActionBlock = "block"
|
||
UnmetActionShowReason = "show_reason"
|
||
|
||
// PrizeType* 是发奖 handler 注册表的键。Stage 1 只实现前三种。
|
||
PrizeTypeVPNDuration = "vpn_duration"
|
||
PrizeTypeCommission = "commission"
|
||
PrizeTypeNone = "none"
|
||
// Stage 2/3 预留。
|
||
PrizeTypeBalance = "balance"
|
||
PrizeTypeGiftAmount = "gift_amount"
|
||
PrizeTypeCoupon = "coupon"
|
||
PrizeTypePoints = "points"
|
||
PrizeTypeEncrypted = "encrypted"
|
||
PrizeTypePhysical = "physical"
|
||
PrizeTypeManualOther = "manual_other"
|
||
|
||
// ChanceSource* 是次数入账触发源。
|
||
ChanceSourceDailySignin = "daily_signin"
|
||
ChanceSourceNewSubscription = "new_subscription"
|
||
ChanceSourceInviteSuccess = "invite_success"
|
||
ChanceSourceManualGrant = "manual_grant"
|
||
|
||
// DispatchState* 是 lottery_draw.dispatch_state 的取值。
|
||
DispatchStateNone = "none"
|
||
DispatchStateAutoClaimed = "auto_claimed"
|
||
DispatchStatePendingClaim = "pending_claim"
|
||
DispatchStatePaid = "paid"
|
||
DispatchStateExpired = "expired"
|
||
DispatchStateFailed = "failed"
|
||
)
|
||
|
||
// ---- 实体 -----------------------------------------------------------------
|
||
|
||
type Activity struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
Title string `gorm:"type:varchar(128);not null;default:'';comment:活动标题"`
|
||
Description string `gorm:"type:text;comment:活动描述"`
|
||
StartAt time.Time `gorm:"not null;comment:开始时间"`
|
||
EndAt time.Time `gorm:"not null;comment:结束时间"`
|
||
Status string `gorm:"type:varchar(16);not null;default:'draft';comment:状态"`
|
||
GridSize int `gorm:"type:tinyint;not null;default:9;comment:九宫格数量"`
|
||
Eligibility string `gorm:"type:json;not null;comment:参与门槛(AND/OR 嵌套规则)"`
|
||
ChanceSources string `gorm:"type:json;not null;comment:次数来源列表"`
|
||
UnmetAction string `gorm:"type:varchar(32);not null;default:'block';comment:未达门槛策略"`
|
||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||
DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"`
|
||
}
|
||
|
||
func (Activity) TableName() string { return "lottery_activity" }
|
||
|
||
type Prize struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:所属活动"`
|
||
Slot int `gorm:"type:tinyint;not null;comment:九宫格位置"`
|
||
Type string `gorm:"type:varchar(32);not null;comment:奖品类型"`
|
||
Name string `gorm:"type:varchar(128);not null;default:'';comment:名称"`
|
||
IconURL string `gorm:"type:varchar(512);not null;default:'';comment:图标 URL"`
|
||
Config string `gorm:"type:json;not null;comment:类型专属配置"`
|
||
Weight int `gorm:"type:int;not null;default:0;comment:加权随机权重"`
|
||
TotalStock sql.NullInt64 `gorm:"type:bigint;comment:总库存(NULL=无限)"`
|
||
RemainingStock sql.NullInt64 `gorm:"type:bigint;comment:剩余库存(NULL=无限)"`
|
||
IsFallback bool `gorm:"type:tinyint(1);not null;default:0;comment:是否为保底奖"`
|
||
Version int64 `gorm:"type:bigint;not null;default:0;comment:乐观锁"`
|
||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||
}
|
||
|
||
func (Prize) TableName() string { return "lottery_prize" }
|
||
|
||
type ChanceBalance struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
UserId int64 `gorm:"type:bigint unsigned;not null;comment:用户 ID"`
|
||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:活动 ID"`
|
||
Remaining int64 `gorm:"type:bigint;not null;default:0;comment:剩余次数"`
|
||
TotalEarned int64 `gorm:"type:bigint;not null;default:0;comment:累计入账"`
|
||
TotalSpent int64 `gorm:"type:bigint;not null;default:0;comment:累计消耗"`
|
||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||
}
|
||
|
||
func (ChanceBalance) TableName() string { return "lottery_chance_balance" }
|
||
|
||
type ChanceGrant struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
UserId int64 `gorm:"type:bigint unsigned;not null;comment:用户 ID"`
|
||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:活动 ID"`
|
||
Source string `gorm:"type:varchar(32);not null;comment:触发源"`
|
||
SourceRef string `gorm:"type:varchar(128);not null;comment:外部业务幂等键"`
|
||
Amount int `gorm:"type:int;not null;default:0;comment:本次发放次数"`
|
||
ExpiresAt *time.Time `gorm:"default:null;comment:到期时间"`
|
||
GrantedAt time.Time `gorm:"<-:create;default:CURRENT_TIMESTAMP;comment:入账时间"`
|
||
}
|
||
|
||
func (ChanceGrant) TableName() string { return "lottery_chance_grant" }
|
||
|
||
type Draw struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
UserId int64 `gorm:"type:bigint unsigned;not null;comment:用户 ID"`
|
||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:活动 ID"`
|
||
ClientNonce string `gorm:"type:varchar(64);not null;comment:前端幂等键"`
|
||
PrizeId sql.NullInt64 `gorm:"type:bigint unsigned;comment:中奖奖品 ID(谢谢参与=NULL)"`
|
||
IsWin bool `gorm:"type:tinyint(1);not null;default:0;comment:是否中奖"`
|
||
DispatchState string `gorm:"type:varchar(16);not null;default:'none';comment:发放状态"`
|
||
DispatchError string `gorm:"type:text;comment:发放失败原因"`
|
||
DispatchedAt *time.Time `gorm:"default:null;comment:发放完成时间"`
|
||
DrawnAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:抽奖时间"`
|
||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||
}
|
||
|
||
func (Draw) TableName() string { return "lottery_draw" }
|
||
|
||
type PrizeSnapshot struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
DrawId int64 `gorm:"type:bigint unsigned;not null;uniqueIndex:uk_draw_id;comment:抽奖记录 ID"`
|
||
PrizeId int64 `gorm:"type:bigint unsigned;not null;comment:奖品 ID"`
|
||
Slot int `gorm:"type:tinyint;not null;comment:九宫格位置"`
|
||
Type string `gorm:"type:varchar(32);not null;comment:奖品类型"`
|
||
Name string `gorm:"type:varchar(128);not null;comment:奖品名称"`
|
||
Config string `gorm:"type:json;not null;comment:类型专属配置"`
|
||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||
}
|
||
|
||
func (PrizeSnapshot) TableName() string { return "lottery_prize_snapshot" }
|
||
|
||
type EligibilitySnapshot struct {
|
||
Id int64 `gorm:"primaryKey"`
|
||
DrawId int64 `gorm:"type:bigint unsigned;not null;uniqueIndex:uk_draw_id;comment:抽奖记录 ID"`
|
||
UserId int64 `gorm:"type:bigint unsigned;not null;comment:用户 ID"`
|
||
ActivityId int64 `gorm:"type:bigint unsigned;not null;comment:活动 ID"`
|
||
Passed bool `gorm:"type:tinyint(1);not null;default:0;comment:是否通过门槛"`
|
||
UnmetReasons string `gorm:"type:json;comment:未通过项"`
|
||
EvaluatedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:评估时间"`
|
||
}
|
||
|
||
func (EligibilitySnapshot) TableName() string { return "lottery_eligibility_snapshot" }
|
||
|
||
// ---- JSON 结构体辅助 -------------------------------------------------------
|
||
|
||
// EligibilityRule 是持久化在 lottery_activity.eligibility 字段里的门槛规则树。
|
||
// 每个节点要么是"叶子"(Type 非空),要么是"聚合"(Op 为 AND/OR + Children)。
|
||
type EligibilityRule struct {
|
||
Op string `json:"op,omitempty"`
|
||
Children []*EligibilityRule `json:"children,omitempty"`
|
||
|
||
Type string `json:"type,omitempty"`
|
||
Params json.RawMessage `json:"params,omitempty"`
|
||
}
|
||
|
||
// ChanceSource 描述次数来源的一条配置,持久化在 lottery_activity.chance_sources。
|
||
type ChanceSource struct {
|
||
Source string `json:"source"`
|
||
Amount int `json:"amount"`
|
||
Params json.RawMessage `json:"params,omitempty"`
|
||
DailyLimit int `json:"daily_limit,omitempty"`
|
||
Unlimited bool `json:"unlimited,omitempty"`
|
||
}
|
||
|
||
// UnmetReason 是门槛评估结果,作为 GET /config 的 unmet_reasons 元素返回。
|
||
type UnmetReason struct {
|
||
Rule string `json:"rule"`
|
||
Hint string `json:"hint"`
|
||
Current int64 `json:"current,omitempty"`
|
||
Required int64 `json:"required,omitempty"`
|
||
}
|