Files
GPTTeamBOT/internal/redeem/redeem.go
Sarteambot Admin 0fde6d4a0b feat: 初始化 ChatGPT Team 管理机器人
核心功能:
- 实现基于 Telegram Inline Button 交互的后台面板与用户端
- 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号
- 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请
- 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码
- 随机邀请功能:成功拉人后自动核销兑换码
- 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误)

系统与配置:
- 使用 PostgreSQL 数据库管理账号、邀请和兑换记录
- 支持在端内动态添加、移除管理员
- 完善 Docker 部署配置与 .gitignore 规则
2026-03-04 20:08:34 +08:00

140 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}