feat: 初始化 ChatGPT Team 管理机器人

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

系统与配置:
- 使用 PostgreSQL 数据库管理账号、邀请和兑换记录
- 支持在端内动态添加、移除管理员
- 完善 Docker 部署配置与 .gitignore 规则
This commit is contained in:
Sarteambot Admin
2026-03-04 20:08:34 +08:00
commit 0fde6d4a0b
19 changed files with 3893 additions and 0 deletions

139
internal/redeem/redeem.go Normal file
View 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)
}