package invite import ( "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "errors" "fmt" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ClaimTask 处理邀请任务奖励领取请求。 func (s *InviteService) ClaimTask(ctx context.Context, user AuthUser, req TaskClaimRequest) (*TaskClaimResponse, error) { if req.RuleID == 0 { return nil, NewAppError(400, "invalid_rule_id", "ruleId is required") } bundle, err := s.loadBundle(ctx, user.SysOrigin) if err != nil { return nil, err } rule := findRuleByID(bundle.Rules, req.RuleID) if rule == nil || !rule.Enabled { return nil, NewAppError(404, "rule_not_found", "rule not found") } if rule.RuleType != ruleTypeInviterValidCount && rule.RuleType != ruleTypeInviterTotalRecharge { return nil, NewAppError(400, "invalid_rule_type", "rule cannot be claimed manually") } monthKey := monthKeyAt(time.Now(), normalizeTimezone(bundle.Config.Timezone)) claimed, err := s.hasTaskClaim(ctx, user.SysOrigin, user.UserID, monthKey, rule.ID) if err != nil { return nil, err } monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, monthKey) if err != nil { return nil, err } progressValue := monthly.TotalRecharge if rule.RuleType == ruleTypeInviterValidCount { progressValue = monthly.InviteCount } if claimed { return &TaskClaimResponse{ Claimed: true, AlreadyClaimed: true, RuleID: rule.ID, RuleType: rule.RuleType, Progress: progressValue, Threshold: rule.ThresholdValue, Reward: rewardFromRule(rule), }, nil } if progressValue < rule.ThresholdValue { return nil, NewAppError(400, "task_not_reached", "task threshold not reached") } // 先发奖,再落领取记录;发奖侧通过 businessKey 兜底幂等。 _, err = s.dispatchReward(ctx, rewardDispatchInput{ SysOrigin: user.SysOrigin, MonthKey: monthKey, InviterUserID: refInt64(user.UserID), TargetUserID: user.UserID, Reward: rewardFromRule(rule), Scene: rule.RuleType, RuleID: refInt64(rule.ID), BusinessKey: fmt.Sprintf("claim:%d:%s:%d", user.UserID, monthKey, rule.ID), GoldOrigin: mapGoldOrigin(rule.RuleType), PropsOrigin: mapPropsOrigin(rule.RuleType), Remark: "resident invite monthly task reward", }) if err != nil { return nil, err } now := time.Now() err = s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.createTaskClaimIfAbsentTx(tx, user.SysOrigin, user.UserID, monthKey, rule.ID, now); err != nil { return err } return nil }) if err != nil { return nil, err } return &TaskClaimResponse{ Claimed: true, AlreadyClaimed: false, RuleID: rule.ID, RuleType: rule.RuleType, Progress: progressValue, Threshold: rule.ThresholdValue, Reward: rewardFromRule(rule), }, nil } // hasTaskClaim 判断任务是否已经被领取。 func (s *InviteService) hasTaskClaim(ctx context.Context, sysOrigin string, inviterUserID int64, monthKey string, ruleID int64) (bool, error) { var count int64 err := s.repo.DB.WithContext(ctx). Model(&model.InviteCampaignTaskClaim{}). Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ? AND rule_id = ?", sysOrigin, inviterUserID, monthKey, ruleID). Count(&count).Error return count > 0, err } // incrementMonthlyProgressTx 在事务里更新月度进度。 func (s *InviteService) incrementMonthlyProgressTx(tx *gorm.DB, sysOrigin string, inviterUserID int64, monthKey string, mutate func(item *model.InviteCampaignMonthlyProgress)) error { var progress model.InviteCampaignMonthlyProgress err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ?", sysOrigin, inviterUserID, monthKey). First(&progress).Error if errors.Is(err, gorm.ErrRecordNotFound) { id, idErr := utils.NextID() if idErr != nil { return idErr } now := time.Now() progress = model.InviteCampaignMonthlyProgress{ ID: id, SysOrigin: sysOrigin, InviterUserID: inviterUserID, MonthKey: monthKey, CreateTime: now, UpdateTime: now, } mutate(&progress) return tx.Create(&progress).Error } if err != nil { return err } mutate(&progress) progress.UpdateTime = time.Now() return tx.Save(&progress).Error } // createTaskClaimIfAbsentTx 在事务里创建领取记录,避免重复领取。 func (s *InviteService) createTaskClaimIfAbsentTx(tx *gorm.DB, sysOrigin string, inviterUserID int64, monthKey string, ruleID int64, claimTime time.Time) error { var existing model.InviteCampaignTaskClaim err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ? AND rule_id = ?", sysOrigin, inviterUserID, monthKey, ruleID). First(&existing).Error if err == nil { return nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } id, idErr := utils.NextID() if idErr != nil { return idErr } return tx.Create(&model.InviteCampaignTaskClaim{ ID: id, SysOrigin: sysOrigin, InviterUserID: inviterUserID, MonthKey: monthKey, RuleID: ruleID, ClaimTime: claimTime, CreateTime: claimTime, UpdateTime: claimTime, }).Error }