package invite import ( "chatapp3-golang/internal/model" "context" "encoding/json" "errors" "strings" "gorm.io/gorm" "gorm.io/gorm/clause" ) // loadBundle 加载某个系统下的邀请活动主配置和启用规则。 func (s *InviteService) loadBundle(ctx context.Context, sysOrigin string) (*campaignBundle, error) { sysOrigin = normalizeSysOrigin(sysOrigin) cacheKey := "invite:campaign:config:" + sysOrigin if raw, err := s.repo.Redis.Get(ctx, cacheKey).Result(); err == nil && raw != "" { var bundle campaignBundle if jsonErr := json.Unmarshal([]byte(raw), &bundle); jsonErr == nil { return &bundle, nil } } bundle := &campaignBundle{ Config: model.InviteCampaignConfig{ SysOrigin: sysOrigin, Enabled: false, Timezone: defaultTimezone, AntiCheatSameIPOnce: true, AntiCheatSameDeviceOnce: true, }, } var configEntity model.InviteCampaignConfig err := s.repo.DB.WithContext(ctx). Where("sys_origin = ?", sysOrigin). First(&configEntity).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } if err == nil { bundle.Config = configEntity if strings.TrimSpace(bundle.Config.Timezone) == "" { bundle.Config.Timezone = defaultTimezone } } if err := s.repo.DB.WithContext(ctx). Where("sys_origin = ?", sysOrigin). Order("sort asc, threshold_value asc, id asc"). Find(&bundle.Rules).Error; err != nil { return nil, err } payload, _ := json.Marshal(bundle) s.repo.Redis.Set(ctx, cacheKey, payload, 0) return bundle, nil } // ensureInviteCode 确保用户拥有邀请码;没有则创建一个新的。 func (s *InviteService) ensureInviteCode(ctx context.Context, user AuthUser) (string, error) { if mapping, err := s.findInviteCodeMappingByUserID(ctx, user.UserID); err == nil { return mapping.InviteCode, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return "", err } if user.Authorization == "" { return "", NewAppError(401, "missing_authorization", "authorization is required to generate invite code") } resp, err := s.java.EnsureInviteCode(ctx, user.Authorization) if err != nil { return "", err } info, infoErr := s.findUserBaseInfo(ctx, user.UserID) if infoErr != nil { return "", infoErr } mapping := model.InviteCodeMapping{ UserID: user.UserID, InviteCode: resp.InviteCode, Account: info.Account, } if err := s.repo.DB.WithContext(ctx). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}}, DoUpdates: clause.AssignmentColumns([]string{"invite_code", "account"}), }). Create(&mapping).Error; err != nil { return "", err } return resp.InviteCode, nil } // findInviteCodeMappingByUserID 按用户 ID 查询邀请码映射。 func (s *InviteService) findInviteCodeMappingByUserID(ctx context.Context, userID int64) (*model.InviteCodeMapping, error) { var mapping model.InviteCodeMapping err := s.repo.DB.WithContext(ctx). Where("user_id = ?", userID). First(&mapping).Error if err != nil { return nil, err } return &mapping, nil } // findInviteCodeMappingByCode 按邀请码查询映射记录。 func (s *InviteService) findInviteCodeMappingByCode(ctx context.Context, inviteCode string) (*model.InviteCodeMapping, error) { var mapping model.InviteCodeMapping err := s.repo.DB.WithContext(ctx). Where("invite_code = ?", inviteCode). First(&mapping).Error if err != nil { return nil, err } return &mapping, nil } // findUserBaseInfo 读取用户基础资料。 func (s *InviteService) findUserBaseInfo(ctx context.Context, userID int64) (*model.UserBaseInfo, error) { var info model.UserBaseInfo err := s.repo.DB.WithContext(ctx). Where("id = ?", userID). First(&info).Error if err != nil { return nil, err } return &info, nil } // findRelationByInvitee 查询某个被邀请人的已绑定关系。 func (s *InviteService) findRelationByInvitee(ctx context.Context, sysOrigin string, inviteeUserID int64) (*model.InviteCampaignRelation, error) { var relation model.InviteCampaignRelation err := s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND invitee_user_id = ?", sysOrigin, inviteeUserID). First(&relation).Error if err != nil { return nil, err } return &relation, nil } // findMonthlyProgress 查询邀请人的当前任务周期进度;底层字段沿用 month_key 以兼容历史表结构。 func (s *InviteService) findMonthlyProgress(ctx context.Context, sysOrigin string, inviterUserID int64, monthKey string) (*model.InviteCampaignMonthlyProgress, error) { var progress model.InviteCampaignMonthlyProgress err := s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ?", sysOrigin, inviterUserID, monthKey). First(&progress).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &model.InviteCampaignMonthlyProgress{ SysOrigin: sysOrigin, InviterUserID: inviterUserID, MonthKey: monthKey, }, nil } if err != nil { return nil, err } return &progress, nil } // loadStats 汇总邀请人的累计邀请统计。 func (s *InviteService) loadStats(ctx context.Context, sysOrigin string, inviterUserID int64) (InviteStats, error) { stats := InviteStats{} if err := s.repo.DB.WithContext(ctx). Model(&model.InviteCampaignRelation{}). Where("sys_origin = ? AND inviter_user_id = ? AND eligible_status = ?", sysOrigin, inviterUserID, relationStatusEligible). Count(&stats.TotalInvitedUsers).Error; err != nil { return stats, err } if err := s.repo.DB.WithContext(ctx).Raw(` SELECT COALESCE(COUNT(1), 0) FROM invite_campaign_invitee_progress ip INNER JOIN invite_campaign_relation r ON r.id = ip.relation_id WHERE r.sys_origin = ? AND r.inviter_user_id = ? AND r.eligible_status = ? AND ip.valid_user = 1 `, sysOrigin, inviterUserID, relationStatusEligible).Scan(&stats.TotalValidUsers).Error; err != nil { return stats, err } if err := s.repo.DB.WithContext(ctx).Raw(` SELECT COALESCE(SUM(ip.total_recharge_coins), 0) FROM invite_campaign_invitee_progress ip INNER JOIN invite_campaign_relation r ON r.id = ip.relation_id WHERE r.sys_origin = ? AND r.inviter_user_id = ? AND r.eligible_status = ? `, sysOrigin, inviterUserID, relationStatusEligible).Scan(&stats.TotalRechargeCoins).Error; err != nil { return stats, err } if err := s.repo.DB.WithContext(ctx).Raw(` SELECT COALESCE(SUM(gold_amount), 0) FROM invite_campaign_reward_log WHERE sys_origin = ? AND inviter_user_id = ? AND target_user_id = ? AND status = ? `, sysOrigin, inviterUserID, inviterUserID, rewardLogStatusSuccess).Scan(&stats.RewardGoldCoins).Error; err != nil { return stats, err } return stats, nil } // loadClaimedRuleIDs 读取某个任务周期已领取的任务规则集合。 func (s *InviteService) loadClaimedRuleIDs(ctx context.Context, sysOrigin string, inviterUserID int64, monthKey string) (map[int64]bool, error) { var claims []model.InviteCampaignTaskClaim if err := s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ?", sysOrigin, inviterUserID, monthKey). Find(&claims).Error; err != nil { return nil, err } result := make(map[int64]bool, len(claims)) for _, item := range claims { result[item.RuleID] = true } return result, nil }