package redeem import ( "fmt" "math/rand" "regexp" "strings" "go-helper/internal/chatgpt" "go-helper/internal/database" "go-helper/internal/model" ) var ( emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`) codeRegex = regexp.MustCompile(`^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`) codeChars = []byte("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") // exclude confusable chars ) // RedeemResult contains information about a successful redemption. type RedeemResult struct { AccountEmail string InviteOK bool Message string } // Redeem validates the code, finds an available account, and sends an invite. func Redeem(db *database.DB, client *chatgpt.Client, code, email string, capacity int) (*RedeemResult, error) { email = strings.TrimSpace(strings.ToLower(email)) code = strings.TrimSpace(strings.ToUpper(code)) if email == "" { return nil, fmt.Errorf("请输入邮箱地址") } if !emailRegex.MatchString(email) { return nil, fmt.Errorf("邮箱格式不正确") } if code == "" { return nil, fmt.Errorf("请输入兑换码") } if !codeRegex.MatchString(code) { return nil, fmt.Errorf("兑换码格式不正确(格式:XXXX-XXXX-XXXX)") } // 1. Look up the code. rc, err := db.GetCodeByCode(code) if err != nil { return nil, fmt.Errorf("兑换码不存在或已失效") } if rc.IsRedeemed { return nil, fmt.Errorf("该兑换码已被使用") } // 2. Find a usable account. var account *model.GptAccount if rc.AccountEmail != "" { // Code is bound to a specific account. accounts, err := db.GetOpenAccounts(capacity + 100) // get all open if err != nil { return nil, fmt.Errorf("查找账号失败: %v", err) } for i := range accounts { if strings.EqualFold(accounts[i].Email, rc.AccountEmail) { if accounts[i].UserCount+accounts[i].InviteCount < capacity { account = &accounts[i] } break } } if account == nil { return nil, fmt.Errorf("该兑换码绑定的账号不可用或已满") } } else { // Find any open account with capacity. accounts, err := db.GetOpenAccounts(capacity) if err != nil || len(accounts) == 0 { return nil, fmt.Errorf("暂无可用账号,请稍后再试") } account = &accounts[0] } // 3. Send invite. inviteErr := client.InviteUser(email, account) // 4. Mark code as redeemed regardless of invite outcome. if err := db.RedeemCode(rc.ID, email); err != nil { return nil, fmt.Errorf("更新兑换码状态失败: %v", err) } // 5. Sync counts. syncCounts(db, client, account) result := &RedeemResult{AccountEmail: account.Email} if inviteErr != nil { result.InviteOK = false result.Message = fmt.Sprintf("兑换成功,但邀请发送失败: %v\n请联系管理员手动添加", inviteErr) } else { result.InviteOK = true result.Message = "兑换成功!邀请邮件已发送到您的邮箱,请查收。" } return result, nil } // GenerateCode creates a random code in XXXX-XXXX-XXXX format. func GenerateCode() string { parts := make([]byte, 12) for i := range parts { parts[i] = codeChars[rand.Intn(len(codeChars))] } return fmt.Sprintf("%s-%s-%s", string(parts[0:4]), string(parts[4:8]), string(parts[8:12])) } // GenerateCodes creates n unique codes. func GenerateCodes(n int) []string { seen := make(map[string]bool) var codes []string for len(codes) < n { c := GenerateCode() if !seen[c] { seen[c] = true codes = append(codes, c) } } return codes } // syncCounts updates user_count and invite_count from the ChatGPT API. func syncCounts(db *database.DB, client *chatgpt.Client, account *model.GptAccount) { userTotal, _, err := client.GetUsers(account) if err != nil { return } inviteTotal, _, err := client.GetInvites(account) if err != nil { return } _ = db.UpdateAccountCounts(account.ID, userTotal, inviteTotal) }