package invite import ( "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "errors" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // dispatchReward 根据 RewardView 类型发放金币或道具奖励。 func (s *InviteService) dispatchReward(ctx context.Context, input rewardDispatchInput) (*rewardDispatchResult, error) { if input.Reward.GoldAmount <= 0 && input.Reward.RewardGroupID == nil { return &rewardDispatchResult{Success: true}, nil } if input.Reward.GoldAmount > 0 { if err := s.dispatchGold(ctx, input); err != nil { return &rewardDispatchResult{Success: false}, err } } if input.Reward.RewardGroupID != nil { if err := s.dispatchProps(ctx, input); err != nil { return &rewardDispatchResult{Success: false}, err } } return &rewardDispatchResult{Success: true}, nil } // dispatchGold 调用钱包接口发放金币奖励。 func (s *InviteService) dispatchGold(ctx context.Context, input rewardDispatchInput) error { businessKey := input.BusinessKey + ":gold" logRecord, err := s.getOrCreateRewardLog(ctx, businessKey, input, input.Reward.GoldAmount, nil) if err != nil { return err } if logRecord.Status == rewardLogStatusSuccess { return nil } err = s.java.GrantGold(ctx, integration.GrantGoldRequest{ UserID: input.TargetUserID, SysOrigin: input.SysOrigin, EventID: businessKey, Origin: input.GoldOrigin, GoldAmount: input.Reward.GoldAmount, Remark: input.Remark, }) if err != nil { if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil { return logErr } return err } if err := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, ""); err != nil { return err } if input.MonthKey != "" && input.InviterUserID != nil && *input.InviterUserID == input.TargetUserID { return s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.incrementMonthlyProgressTx(tx, input.SysOrigin, input.TargetUserID, input.MonthKey, func(item *model.InviteCampaignMonthlyProgress) { item.RewardGoldCoins += input.Reward.GoldAmount }) }) } return nil } // dispatchProps 调用道具奖励接口发放道具奖励。 func (s *InviteService) dispatchProps(ctx context.Context, input rewardDispatchInput) error { if input.Reward.RewardGroupID == nil { return nil } businessKey := input.BusinessKey + ":props" logRecord, err := s.getOrCreateRewardLog(ctx, businessKey, input, 0, input.Reward.RewardGroupID) if err != nil { return err } if logRecord.Status == rewardLogStatusSuccess { return nil } err = s.java.GrantProps(ctx, integration.GrantPropsRequest{ TrackID: logRecord.ID, AcceptUserID: input.TargetUserID, SysOrigin: input.SysOrigin, Origin: input.PropsOrigin, SourceGroupID: *input.Reward.RewardGroupID, }) if err != nil { if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil { return logErr } return err } return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, "") } // getOrCreateRewardLog 创建或复用发奖日志,保证同 businessKey 幂等。 func (s *InviteService) getOrCreateRewardLog(ctx context.Context, businessKey string, input rewardDispatchInput, goldAmount int64, rewardGroupID *int64) (*model.InviteCampaignRewardLog, error) { var record model.InviteCampaignRewardLog err := s.repo.DB.WithContext(ctx). Where("business_key = ?", businessKey). First(&record).Error if err == nil { return &record, nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } id, idErr := utils.NextID() if idErr != nil { return nil, idErr } now := time.Now() record = model.InviteCampaignRewardLog{ ID: id, SysOrigin: input.SysOrigin, RelationID: input.RelationID, InviterUserID: input.InviterUserID, InviteeUserID: input.InviteeUserID, TargetUserID: input.TargetUserID, RuleID: input.RuleID, RewardScene: input.Scene, BusinessKey: businessKey, GoldAmount: goldAmount, RewardGroupID: rewardGroupID, Status: rewardLogStatusPending, CreateTime: now, UpdateTime: now, } if err := s.repo.DB.WithContext(ctx).Create(&record).Error; err != nil { if !strings.Contains(strings.ToLower(err.Error()), "duplicate") { return nil, err } if err := s.repo.DB.WithContext(ctx).Where("business_key = ?", businessKey).First(&record).Error; err != nil { return nil, err } } return &record, nil } // updateRewardLogStatus 更新发奖日志状态和错误信息。 func (s *InviteService) updateRewardLogStatus(ctx context.Context, id int64, status, message string) error { return s.repo.DB.WithContext(ctx). Model(&model.InviteCampaignRewardLog{}). Where("id = ?", id). Updates(map[string]any{ "status": status, "error_message": truncate(message, 255), "update_time": time.Now(), }).Error } // mergeRewardedRuleIDs 把新命中的规则 ID 合并到进度表。 func (s *InviteService) mergeRewardedRuleIDs(ctx context.Context, relationID int64, ruleIDs []int64) error { return s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var progress model.InviteCampaignInviteeProgress if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("relation_id = ?", relationID). First(&progress).Error; err != nil { return err } current := parseRewardedRuleIDs(progress.RewardedRuleIDs) for _, ruleID := range ruleIDs { current[ruleID] = struct{}{} } progress.RewardedRuleIDs = formatRewardedRuleIDs(current) progress.UpdateTime = time.Now() return tx.Save(&progress).Error }) } // optionalRuleID 把规则指针安全转换成规则 ID 指针。 func optionalRuleID(rule *model.InviteCampaignRewardRule) *int64 { if rule == nil || rule.ID == 0 { return nil } return refInt64(rule.ID) } // refInt64 返回 int64 指针。 func refInt64(value int64) *int64 { return &value } // refTime 返回 time.Time 指针。 func refTime(value time.Time) *time.Time { return &value } // mapGoldOrigin 把规则类型映射成金币发奖来源。 func mapGoldOrigin(ruleType string) string { switch ruleType { case ruleTypeInviterBind: return "INVITED_NEW_USER_REWARD" case ruleTypeInviteeRecharge: return "INVITED_USER_FIRST_RECHARGE_REWARD" case ruleTypeInviterTotalRecharge: return "CUMULATIVE_RECHARGE_REWARDS" default: return "INVITE_USER_REWARDS" } } // mapPropsOrigin 把规则类型映射成道具发奖来源。 func mapPropsOrigin(ruleType string) string { switch ruleType { case ruleTypeInviterTotalRecharge: return "CUMULATIVE_RECHARGE_REWARDS" default: return "INVITE_USER_REWARDS" } }