package services import ( "fmt" "math/rand" "strings" "time" "goalfymax-admin/internal/models" "goalfymax-admin/internal/storage" ) type InviteCodeService interface { Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error) GetByID(id uint) (*models.InviteCode, error) GetByCode(code string) (*models.InviteCode, error) List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error) Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error) Delete(id uint) error GetStatistics() (*models.InviteCodeStatistics, error) MarkAsUsed(code string) error ValidateInviteCode(code string) error } type inviteCodeService struct { storage storage.InviteCodeStorage emailService *EmailService } func NewInviteCodeService(storage storage.InviteCodeStorage) InviteCodeService { return &inviteCodeService{ storage: storage, emailService: NewEmailService(), } } func (s *inviteCodeService) Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error) { if req == nil { return nil, fmt.Errorf("请求参数不能为空") } // 设置过期时间 var expiresAt *time.Time if req.ExpiresAt != nil { expiresAt = req.ExpiresAt } else { // 默认30天后过期 defaultExpiry := time.Now().AddDate(0, 0, 30) expiresAt = &defaultExpiry } // 设置用户等级ID,如果未提供则默认为1 var userLevelID *uint if req.UserLevelID != nil { userLevelID = req.UserLevelID } else { defaultUserLevelID := uint(1) userLevelID = &defaultUserLevelID } // 如果提供了邮箱列表,为每个邮箱创建一个邀请码 if len(req.Emails) > 0 { // 对邮箱列表进行去重处理 emailMap := make(map[string]bool) uniqueEmails := make([]string, 0, len(req.Emails)) for _, email := range req.Emails { // 去除空格并转换为小写进行去重 email = strings.TrimSpace(strings.ToLower(email)) if email == "" { continue } // 如果邮箱已存在,跳过 if emailMap[email] { continue } emailMap[email] = true uniqueEmails = append(uniqueEmails, email) } var inviteCodes []*models.InviteCode for _, email := range uniqueEmails { // 生成唯一邀请码 var code string for { code = s.generateCode() existingCode, err := s.storage.GetByCode(code) if err != nil || existingCode == nil { break // 邀请码不存在,可以使用 } } inviteCode := &models.InviteCode{ Code: code, ClientID: req.ClientID, Email: email, UserLevelID: userLevelID, ExpiresAt: expiresAt, CreatedAt: time.Now(), } if err := s.storage.Create(inviteCode); err != nil { return nil, fmt.Errorf("创建邀请码失败: %w", err) } // 发送邀请邮件(如果提供了邮箱) // 使用 goroutine 异步发送,避免阻塞 if email != "" && s.emailService != nil { go func(emailAddr, code string) { // 使用默认语言 "zh" 发送邮件 if err := s.emailService.SendInviteCodeApprovalEmail(emailAddr, code, "zh", expiresAt); err != nil { // 记录错误但不影响创建流程,邮件发送失败不影响邀请码创建 fmt.Printf("[InviteCodeService] 发送邀请邮件失败: %v (邀请码: %s, 邮箱: %s)\n", err, code, emailAddr) } else { fmt.Printf("[InviteCodeService] 邀请邮件发送成功 (邀请码: %s, 邮箱: %s)\n", code, emailAddr) } }(email, inviteCode.Code) } inviteCodes = append(inviteCodes, inviteCode) } if len(inviteCodes) == 0 { return nil, fmt.Errorf("没有有效的邮箱地址") } return inviteCodes, nil } // 如果没有提供邮箱,只创建一个邀请码(向后兼容) var code string for { code = s.generateCode() existingCode, err := s.storage.GetByCode(code) if err != nil || existingCode == nil { break // 邀请码不存在,可以使用 } } inviteCode := &models.InviteCode{ Code: code, ClientID: req.ClientID, Email: "", UserLevelID: userLevelID, ExpiresAt: expiresAt, CreatedAt: time.Now(), } if err := s.storage.Create(inviteCode); err != nil { return nil, fmt.Errorf("创建邀请码失败: %w", err) } return []*models.InviteCode{inviteCode}, nil } func (s *inviteCodeService) GetByID(id uint) (*models.InviteCode, error) { return s.storage.GetByID(id) } func (s *inviteCodeService) GetByCode(code string) (*models.InviteCode, error) { return s.storage.GetByCode(code) } func (s *inviteCodeService) List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error) { inviteCodes, total, err := s.storage.List(req) if err != nil { return nil, fmt.Errorf("获取邀请码列表失败: %w", err) } return &models.InviteCodeListResponse{ List: inviteCodes, Total: total, }, nil } func (s *inviteCodeService) Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error) { // 获取现有邀请码 inviteCode, err := s.storage.GetByID(id) if err != nil { return nil, fmt.Errorf("邀请码不存在") } // 检查是否已被使用 if inviteCode.IsUsed { return nil, fmt.Errorf("已使用的邀请码无法修改") } // 更新客户端ID if req != nil && req.ClientID != "" { inviteCode.ClientID = req.ClientID } // 更新邮箱 if req != nil && req.Email != "" { inviteCode.Email = req.Email } // 更新用户等级ID if req != nil && req.UserLevelID != nil { inviteCode.UserLevelID = req.UserLevelID } // 更新过期时间 if req != nil && req.ExpiresAt != nil { inviteCode.ExpiresAt = req.ExpiresAt } // 保存更新 if err := s.storage.Update(inviteCode); err != nil { return nil, fmt.Errorf("更新邀请码失败: %w", err) } return inviteCode, nil } func (s *inviteCodeService) Delete(id uint) error { _, err := s.storage.GetByID(id) if err != nil { return fmt.Errorf("邀请码不存在") } if err := s.storage.Delete(id); err != nil { return fmt.Errorf("删除邀请码失败: %w", err) } return nil } func (s *inviteCodeService) GetStatistics() (*models.InviteCodeStatistics, error) { return s.storage.GetStatistics() } func (s *inviteCodeService) MarkAsUsed(code string) error { inviteCode, err := s.storage.GetByCode(code) if err != nil { return fmt.Errorf("邀请码不存在") } if inviteCode.IsUsed { return fmt.Errorf("邀请码已被使用") } // 检查是否过期 if s.storage.IsExpired(inviteCode) { return fmt.Errorf("邀请码已过期") } inviteCode.IsUsed = true if err := s.storage.Update(inviteCode); err != nil { return fmt.Errorf("标记邀请码为已使用失败: %w", err) } return nil } // ValidateInviteCode 验证邀请码是否有效(未使用且未过期) func (s *inviteCodeService) ValidateInviteCode(code string) error { inviteCode, err := s.storage.GetByCode(code) if err != nil { return fmt.Errorf("邀请码不存在") } if inviteCode.IsUsed { return fmt.Errorf("邀请码已被使用") } // 检查是否过期 if s.storage.IsExpired(inviteCode) { return fmt.Errorf("邀请码已过期") } return nil } func (s *inviteCodeService) generateCode() string { chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // 生成8位随机字符 part1 := make([]byte, 8) for i := range part1 { part1[i] = chars[rand.Intn(len(chars))] } // 生成4位随机字符 part2 := make([]byte, 4) for i := range part2 { part2[i] = chars[rand.Intn(len(chars))] } return fmt.Sprintf("GFY-%s-%s", string(part1), string(part2)) }