// Package lotteryLogic 承载抽奖相关的 asynq 后台任务。 // Stage 2 唯一一个后台任务:每小时扫描 pending_claim 且已过期的领奖工单, // 状态推到 expired。业务规则:"过期不补次数",所以只更新 claim/draw,不做补偿。 package lotteryLogic import ( "context" "time" "github.com/hibiken/asynq" modelLottery "github.com/perfect-panel/server/internal/model/lottery" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/logger" ) // ExpireClaimLogic 扫描已过期但状态仍是 pending_claim 的 lottery_claim // (用户没在窗口内提交领奖信息),把它们推到 expired 终态。同时把关联的 // lottery_draw.dispatch_state 也推到 expired,让 GET /records 与后台视图一致。 type ExpireClaimLogic struct { svc *svc.ServiceContext } func NewExpireClaimLogic(svc *svc.ServiceContext) *ExpireClaimLogic { return &ExpireClaimLogic{svc: svc} } // ProcessTask 每小时被 asynq scheduler 触发。 // // SQL 采用两步: // 1. UPDATE lottery_claim SET status='expired' WHERE status='pending_claim' AND expires_at < now // 2. UPDATE lottery_draw SET dispatch_state='expired' // WHERE dispatch_state='pending_claim' // AND id IN (SELECT draw_id FROM lottery_claim WHERE status='expired' AND ...) // // 步骤 1 由 lottery_claim.status 的语义主导;步骤 2 是为了避免 draw 视图脱节。 // 单次 batch 不设上限——线上 pending_claim 过期量级远小于 asynq 单任务的处理窗口, // 若未来量级上来再改成 loop + LIMIT。 func (l *ExpireClaimLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { now := time.Now() claimRes := l.svc.DB.WithContext(ctx). Model(&modelLottery.Claim{}). Where("status = ? AND expires_at < ?", modelLottery.ClaimStatusPendingClaim, now). Update("status", modelLottery.ClaimStatusExpired) if claimRes.Error != nil { logger.WithContext(ctx).Error("[LotteryExpireClaim] update lottery_claim failed", logger.Field("error", claimRes.Error.Error()), ) return claimRes.Error } if claimRes.RowsAffected == 0 { return nil } drawRes := l.svc.DB.WithContext(ctx). Model(&modelLottery.Draw{}). Where("dispatch_state = ? AND id IN (?)", modelLottery.DispatchStatePendingClaim, l.svc.DB.WithContext(ctx). Model(&modelLottery.Claim{}). Select("draw_id"). Where("status = ?", modelLottery.ClaimStatusExpired)). Update("dispatch_state", modelLottery.DispatchStateExpired) if drawRes.Error != nil { // draw 更新失败不阻断——status 已 expired,缺 draw 一致性下次跑还能补上。 logger.WithContext(ctx).Error("[LotteryExpireClaim] update lottery_draw failed", logger.Field("error", drawRes.Error.Error()), logger.Field("claim_expired_count", claimRes.RowsAffected), ) } logger.WithContext(ctx).Info("[LotteryExpireClaim] expired claims swept", logger.Field("claim_expired_count", claimRes.RowsAffected), logger.Field("draw_updated_count", drawRes.RowsAffected), ) return nil }