feat: 初始化 ChatGPT Team 管理机器人
核心功能: - 实现基于 Telegram Inline Button 交互的后台面板与用户端 - 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号 - 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请 - 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码 - 随机邀请功能:成功拉人后自动核销兑换码 - 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误) 系统与配置: - 使用 PostgreSQL 数据库管理账号、邀请和兑换记录 - 支持在端内动态添加、移除管理员 - 完善 Docker 部署配置与 .gitignore 规则
This commit is contained in:
139
internal/redeem/redeem.go
Normal file
139
internal/redeem/redeem.go
Normal file
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user