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 已实装:vpn_duration / commission / none。 // Stage 2 新增人工奖:crypto / physical / manual_other。 PrizeTypeVPNDuration = "vpn_duration" PrizeTypeCommission = "commission" PrizeTypeNone = "none" // Stage 2 人工奖类型(HIF-4)。 PrizeTypeCrypto = "crypto" PrizeTypePhysical = "physical" PrizeTypeManualOther = "manual_other" // Stage 3 预留。 PrizeTypeBalance = "balance" PrizeTypeGiftAmount = "gift_amount" PrizeTypeCoupon = "coupon" PrizeTypePoints = "points" // 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" // ClaimStatus* 是 lottery_claim.status 的取值(Stage 2)。 // pending_claim: 已入库,等用户填领奖信息。 // reviewing: 用户已提交,等运营审核。 // paying: 运营 approve,等运营线下打款/发货 + mark-paid。 // paid: 运营已录入 tx_hash / delivery_ref。终态。 // rejected: 运营 reject(可为 reviewing → rejected 或 paying → rejected);用户可再次提交。 // expired: pending_claim 超时未提交(业务规则:过期不补次数)。终态。 ClaimStatusPendingClaim = "pending_claim" ClaimStatusReviewing = "reviewing" ClaimStatusPaying = "paying" ClaimStatusPaid = "paid" ClaimStatusRejected = "rejected" ClaimStatusExpired = "expired" // DefaultClaimTTLHours 是 Stage 2 spec 里"默认 7 天"的实际编码: // 每个奖品可通过 config.claim_ttl_hours 覆盖单个奖品的过期窗口。 DefaultClaimTTLHours = 24 * 7 ) // ---- 实体 ----------------------------------------------------------------- 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:8;comment:九宫格数量(HIF-4 F8:布局 A 3×3 挖中心 → 8 个奖品格)"` 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" } // Claim 是 Stage 2 的人工奖领奖工单。lottery_draw ↔ lottery_claim 一对一 // (由 lottery_claim.draw_id UNIQUE 保证)。 // // 生命周期:抽奖事务命中人工类奖品 → 同事务插入一行 status=pending_claim; // 用户 POST /claim → 转 reviewing;运营 approve → paying → mark-paid → paid。 // 详细状态机见 02159_lottery_claim.up.sql 的注释。 type Claim 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"` PrizeType string `gorm:"type:varchar(32);not null;comment:奖品类型(冗余便于后台过滤)"` ClaimData string `gorm:"type:json;comment:用户提交的领奖表单(结构随 prize_type 变化)"` Status string `gorm:"type:varchar(32);not null;default:'pending_claim';comment:状态"` SubmittedAt *time.Time `gorm:"default:null;comment:用户提交领奖信息时间"` ExpiresAt time.Time `gorm:"not null;comment:领奖窗口截止时间"` ReviewedBy int64 `gorm:"type:bigint unsigned;default:0;comment:最近一次审核操作者"` ReviewedAt *time.Time `gorm:"default:null;comment:最近一次审核时间"` RejectReason string `gorm:"type:varchar(512);not null;default:'';comment:拒绝原因"` TxHash string `gorm:"type:varchar(128);not null;default:'';comment:链上交易哈希"` DeliveryRef string `gorm:"type:varchar(128);not null;default:'';comment:快递单号 / 发货单据编号"` PaidAt *time.Time `gorm:"default:null;comment:运营标记打款/发货完成时间"` CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } // TableName 对齐 02159 migration。 func (Claim) TableName() string { return "lottery_claim" } // IsClaimStatusResubmittable 判断当前 claim 状态是否允许用户再次提交领奖数据。 // pending_claim(还没提交过)与 rejected(被运营拒绝后允许重填)算入。 // 其他状态(reviewing / paying / paid / expired)都禁止再提交。 func IsClaimStatusResubmittable(status string) bool { return status == ClaimStatusPendingClaim || status == ClaimStatusRejected } // IsPrizeTypeManualClaim 判断某奖品类型是否属于"人工领奖"类别, // 即 draw 时需要挂 lottery_claim 而不是自动发放。 func IsPrizeTypeManualClaim(prizeType string) bool { switch prizeType { case PrizeTypeCrypto, PrizeTypePhysical, PrizeTypeManualOther: return true } return false } // ---- 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"` }