From 42c423bd329f6e9aef62cfb72d6494f934266ca1 Mon Sep 17 00:00:00 2001 From: sar Date: Tue, 13 Jan 2026 14:42:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20ChatGPT?= =?UTF-8?q?=20Team=20=E7=AE=A1=E7=90=86=E5=90=8E=E7=AB=AF=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加用户认证模块 (JWT + 密码管理) - 添加 ChatGPT 账户管理功能 - 添加卡密管理功能 (创建、批量生成、查询) - 添加邀请功能 - 配置数据库迁移和路由系统 --- .env.example | 18 + .gitignore | 39 ++ cmd/main.go | 43 +++ cmd/seed/main.go | 60 +++ database_schema.md | 156 ++++++++ go.mod | 9 + go.sum | 6 + internal/auth/jwt.go | 66 ++++ internal/auth/password.go | 17 + internal/config/env.go | 49 +++ internal/db/db.go | 99 +++++ internal/db/migrations.go | 204 ++++++++++ internal/handler/auth_handler.go | 138 +++++++ internal/handler/card_key_handler.go | 213 +++++++++++ internal/handler/chatgpt_account_handler.go | 287 ++++++++++++++ internal/handler/invite_handler.go | 393 ++++++++++++++++++++ internal/middleware/auth_middleware.go | 65 ++++ internal/models/admin.go | 23 ++ internal/models/api_key.go | 25 ++ internal/models/card_key.go | 42 +++ internal/models/chatgpt_account.go | 109 ++++++ internal/models/invitation.go | 43 +++ internal/models/system_setting.go | 42 +++ internal/repository/admin_repo.go | 120 ++++++ internal/repository/card_key_repo.go | 89 +++++ internal/repository/chatgpt_account_repo.go | 207 +++++++++++ internal/repository/invitation_repo.go | 83 +++++ internal/router/router.go | 95 +++++ internal/service/chatgpt_service.go | 229 ++++++++++++ 29 files changed, 2969 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 cmd/main.go create mode 100644 cmd/seed/main.go create mode 100644 database_schema.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/auth/jwt.go create mode 100644 internal/auth/password.go create mode 100644 internal/config/env.go create mode 100644 internal/db/db.go create mode 100644 internal/db/migrations.go create mode 100644 internal/handler/auth_handler.go create mode 100644 internal/handler/card_key_handler.go create mode 100644 internal/handler/chatgpt_account_handler.go create mode 100644 internal/handler/invite_handler.go create mode 100644 internal/middleware/auth_middleware.go create mode 100644 internal/models/admin.go create mode 100644 internal/models/api_key.go create mode 100644 internal/models/card_key.go create mode 100644 internal/models/chatgpt_account.go create mode 100644 internal/models/invitation.go create mode 100644 internal/models/system_setting.go create mode 100644 internal/repository/admin_repo.go create mode 100644 internal/repository/card_key_repo.go create mode 100644 internal/repository/chatgpt_account_repo.go create mode 100644 internal/repository/invitation_repo.go create mode 100644 internal/router/router.go create mode 100644 internal/service/chatgpt_service.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..99ac588 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# 数据库配置 +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=your_password +DB_NAME=gpt_manager +DB_SSLMODE=disable + +# JWT 配置 +JWT_SECRET=your-secret-key-change-in-production + +# 服务配置 +PORT=8080 + +# 默认管理员配置 (首次运行时创建) +ADMIN_USERNAME=admin +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=admin123 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..114d8b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Environment files +.env +.env.local +.env.*.local + +# Binary files +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Build output +/bin/ +/dist/ +/build/ + +# Go specific +/vendor/ +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Test coverage +coverage.out +coverage.html diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..73dbaa0 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "net/http" + "os" + + "gpt-manager-go/internal/config" + "gpt-manager-go/internal/db" + "gpt-manager-go/internal/router" +) + +func main() { + // 加载 .env 文件 + if err := config.LoadEnv(".env"); err != nil { + log.Printf("Warning: Failed to load .env file: %v", err) + } + + // 初始化数据库连接 + if err := db.Init(); err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer db.Close() + + // 执行数据库迁移 + if err := db.Migrate(db.DB); err != nil { + log.Fatalf("Failed to run migrations: %v", err) + } + + // 设置路由 + handler := router.SetupRoutes(db.DB) + + // 获取端口 + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Server starting on port %s...", port) + if err := http.ListenAndServe(":"+port, handler); err != nil { + log.Fatalf("Server failed: %v", err) + } +} diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..709c4d0 --- /dev/null +++ b/cmd/seed/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "log" + + "gpt-manager-go/internal/auth" + "gpt-manager-go/internal/config" + "gpt-manager-go/internal/db" + "gpt-manager-go/internal/models" + "gpt-manager-go/internal/repository" +) + +func main() { + // 加载配置 + config.LoadEnv(".env") + + // 连接数据库 + if err := db.Init(); err != nil { + log.Fatalf("Failed to connect database: %v", err) + } + defer db.Close() + + // 执行迁移 + if err := db.Migrate(db.DB); err != nil { + log.Fatalf("Failed to migrate: %v", err) + } + + // 创建测试管理员 + adminRepo := repository.NewAdminRepository(db.DB) + + // 检查是否已存在 + existing, _ := adminRepo.FindByUsername("admin") + if existing != nil { + fmt.Println("Admin user already exists!") + return + } + + // 创建密码哈希 + hash, err := auth.HashPassword("admin123") + if err != nil { + log.Fatalf("Failed to hash password: %v", err) + } + + admin := &models.Admin{ + Username: "admin", + Email: "admin@example.com", + PasswordHash: hash, + IsSuperAdmin: true, + IsActive: true, + } + + if err := adminRepo.Create(admin); err != nil { + log.Fatalf("Failed to create admin: %v", err) + } + + fmt.Println("✅ Test admin created successfully!") + fmt.Println(" Username: admin") + fmt.Println(" Password: admin123") +} diff --git a/database_schema.md b/database_schema.md new file mode 100644 index 0000000..a77b4c6 --- /dev/null +++ b/database_schema.md @@ -0,0 +1,156 @@ +# GPT Manager 数据库表结构 + +此项目共有 **6 张表**,用于实现 ChatGPT Team 账号管理与邀请系统。 + +--- + +## 1. `admins` - 管理员表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `username` | String(50) | UNIQUE, NOT NULL, INDEX | 用户名 | +| `email` | String(120) | UNIQUE, NOT NULL, INDEX | 邮箱 | +| `password_hash` | String(128) | NOT NULL | 密码哈希 (bcrypt) | +| `is_super_admin` | Boolean | NOT NULL, DEFAULT FALSE | 是否超级管理员 | +| `is_active` | Boolean | NOT NULL, DEFAULT TRUE | 是否激活 | +| `created_at` | DateTime | NOT NULL | 创建时间 | +| `last_login` | DateTime | NULLABLE | 最后登录时间 | + +**关系**: → `card_keys` (一对多) / → `api_keys` (一对多) + +--- + +## 2. `chatgpt_accounts` - ChatGPT Team 账号表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `name` | String(100) | NOT NULL | 账号名称 | +| `email` | String(120) | NOT NULL | 邮箱地址 | +| `auth_token` | Text | NOT NULL | 授权 Token | +| `team_account_id` | String(100) | NOT NULL | Team Account ID (UUID) | +| `max_invitations` | Integer | NOT NULL, DEFAULT 50 | 最大邀请数 | +| `current_invitations` | Integer | NOT NULL, DEFAULT 0 | 当前已用邀请数 | +| `is_active` | Boolean | NOT NULL, DEFAULT TRUE, INDEX | 是否激活 | +| `consecutive_failures` | Integer | NOT NULL, DEFAULT 0 | 连续失败次数 | +| `last_check` | DateTime | NULLABLE | 最后检测时间 | +| `last_used` | DateTime | NULLABLE | 最后使用时间 | +| `created_at` | DateTime | NOT NULL | 创建时间 | +| `updated_at` | DateTime | NULLABLE | 更新时间 | + +**关系**: → `invitations` (一对多) + +--- + +## 3. `card_keys` - 卡密表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `key` | String(19) | UNIQUE, NOT NULL, INDEX | 卡密 (XXXX-XXXX-XXXX-XXXX) | +| `max_uses` | Integer | NOT NULL, DEFAULT 1 | 最大使用次数 | +| `used_count` | Integer | NOT NULL, DEFAULT 0 | 已使用次数 | +| `validity_type` | String(20) | NOT NULL | 有效期类型 (month/quarter/year/custom) | +| `expires_at` | DateTime | NOT NULL, INDEX | 过期时间 | +| `is_active` | Boolean | NOT NULL, DEFAULT TRUE, INDEX | 是否激活 | +| `created_by_id` | Integer | FK → `admins.id`, NOT NULL | 创建者 | +| `created_at` | DateTime | NOT NULL | 创建时间 | + +**关系**: → `invitations` (一对多) + +--- + +## 4. `invitations` - 邀请记录表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `card_key_id` | Integer | FK → `card_keys.id`, NULLABLE, INDEX | 关联卡密 | +| `account_id` | Integer | FK → `chatgpt_accounts.id`, NOT NULL, INDEX | 关联账号 | +| `invited_email` | String(120) | NOT NULL, INDEX | 被邀请邮箱 | +| `status` | String(20) | NOT NULL, DEFAULT 'pending', INDEX | 状态 | +| `error_message` | Text | NULLABLE | 错误信息 | +| `expires_at` | DateTime | NULLABLE | 邀请过期时间 | +| `created_at` | DateTime | NOT NULL, INDEX | 创建时间 | +| `updated_at` | DateTime | NULLABLE | 更新时间 | +| `api_response` | Text | NULLABLE | API 响应 (JSON) | + +**状态值**: `pending` / `sent` / `accepted` / `failed` / `expired` + +--- + +## 5. `api_keys` - API 密钥表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `key` | String(64) | UNIQUE, NOT NULL, INDEX | API Key (sk_live_xxx) | +| `name` | String(100) | NOT NULL | 名称 | +| `created_by_id` | Integer | FK → `admins.id`, NOT NULL | 创建者 | +| `is_active` | Boolean | NOT NULL, DEFAULT TRUE, INDEX | 是否激活 | +| `rate_limit` | Integer | NOT NULL, DEFAULT 60 | 速率限制 (次/分钟) | +| `allowed_ips` | Text | DEFAULT '[]' | IP 白名单 (JSON 数组) | +| `last_used` | DateTime | NULLABLE | 最后使用时间 | +| `request_count` | Integer | NOT NULL, DEFAULT 0 | 请求计数 | +| `created_at` | DateTime | NOT NULL | 创建时间 | + +--- + +## 6. `system_settings` - 系统配置表 + +| 字段名 | 类型 | 约束 | 说明 | +|--------|------|------|------| +| `id` | Integer | PK | 主键 | +| `key` | String(100) | UNIQUE, NOT NULL, INDEX | 配置键名 | +| `value` | Text | NOT NULL | 配置值 | +| `value_type` | String(20) | NOT NULL, DEFAULT 'string' | 值类型 (string/int/float/bool/json) | +| `description` | String(255) | NULLABLE | 配置描述 | +| `updated_at` | DateTime | NULLABLE | 更新时间 | + +**默认配置项**: +| 键名 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `turnstile_enabled` | bool | false | Cloudflare Turnstile 开关 | +| `turnstile_site_key` | string | '' | Turnstile Site Key | +| `turnstile_secret_key` | string | '' | Turnstile Secret Key | +| `token_check_interval` | int | 6 | Token 检测间隔(小时)| +| `token_failure_threshold` | int | 2 | 连续失败禁用阈值 | +| `invitation_validity_days` | int | 30 | 邀请有效期(天)| +| `site_title` | string | 'ChatGPT Team 邀请' | 站点标题 | + +--- + +## ER 关系图 + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ +│ admins │ │ chatgpt_accounts │ │ card_keys │ +├─────────────┤ ├──────────────────┤ ├─────────────┤ +│ id (PK) │───┐ │ id (PK) │───┐ │ id (PK) │ +│ username │ │ │ name │ │ │ key │ +│ email │ │ │ auth_token │ │ │ max_uses │ +│ password │ │ │ team_account_id │ │ │ expires_at │ +└─────────────┘ │ └──────────────────┘ │ └─────────────┘ + │ │ │ │ │ + │ │ │ │ │ + ▼ │ ▼ │ ▼ +┌─────────────┐ │ ┌──────────────────┐ │ │ +│ api_keys │ │ │ invitations │◄──┴─────────┘ +├─────────────┤ │ ├──────────────────┤ +│ id (PK) │ │ │ id (PK) │ +│ key │ │ │ account_id (FK) │ +│created_by_id│◄──┘ │ card_key_id (FK) │ +└─────────────┘ │ invited_email │ + │ status │ + └──────────────────┘ + +┌──────────────────┐ +│ system_settings │ +├──────────────────┤ +│ id (PK) │ +│ key │ +│ value │ +│ value_type │ +└──────────────────┘ +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1332579 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module gpt-manager-go + +go 1.24.0 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.47.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f5c9292 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go new file mode 100644 index 0000000..9fe66a6 --- /dev/null +++ b/internal/auth/jwt.go @@ -0,0 +1,66 @@ +package auth + +import ( + "errors" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte(getJWTSecret()) + +// Claims JWT 声明 +type Claims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + IsSuperAdmin bool `json:"is_super_admin"` + jwt.RegisteredClaims +} + +// GenerateToken 生成 JWT Token +func GenerateToken(userID int, username string, isSuperAdmin bool) (string, error) { + expireHours := 24 // Token 有效期 24 小时 + + claims := &Claims{ + UserID: userID, + Username: username, + IsSuperAdmin: isSuperAdmin, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "gpt-manager", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +// ParseToken 解析 JWT Token +func ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("invalid signing method") + } + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} + +func getJWTSecret() string { + if secret := os.Getenv("JWT_SECRET"); secret != "" { + return secret + } + return "your-default-secret-key-change-in-production" +} diff --git a/internal/auth/password.go b/internal/auth/password.go new file mode 100644 index 0000000..7dccd20 --- /dev/null +++ b/internal/auth/password.go @@ -0,0 +1,17 @@ +package auth + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPassword 对密码进行 bcrypt 哈希 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword 验证密码 +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/internal/config/env.go b/internal/config/env.go new file mode 100644 index 0000000..2b3dc01 --- /dev/null +++ b/internal/config/env.go @@ -0,0 +1,49 @@ +package config + +import ( + "bufio" + "os" + "strings" +) + +// LoadEnv 从 .env 文件加载环境变量 +func LoadEnv(filename string) error { + file, err := os.Open(filename) + if err != nil { + // 文件不存在时不报错,直接使用系统环境变量 + if os.IsNotExist(err) { + return nil + } + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // 跳过空行和注释 + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 解析 KEY=VALUE + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + // 移除引号 + value = strings.Trim(value, `"'`) + + // 只在环境变量未设置时才设置 + if os.Getenv(key) == "" { + os.Setenv(key, value) + } + } + + return scanner.Err() +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..f189c5c --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,99 @@ +package db + +import ( + "database/sql" + "fmt" + "log" + "os" + "time" + + _ "github.com/lib/pq" +) + +var DB *sql.DB + +// Config 数据库配置 +type Config struct { + Host string + Port int + User string + Password string + DBName string + SSLMode string +} + +// DefaultConfig 从环境变量读取默认配置 +func DefaultConfig() Config { + return Config{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnvInt("DB_PORT", 5432), + User: getEnv("DB_USER", "postgres"), + Password: getEnv("DB_PASSWORD", ""), + DBName: getEnv("DB_NAME", "gpt_manager"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + } +} + +// Connect 连接数据库 +func Connect(cfg Config) (*sql.DB, error) { + dsn := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, + ) + + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // 配置连接池 + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + + // 测试连接 + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + log.Println("Database connected successfully") + return db, nil +} + +// Init 初始化全局数据库连接 +func Init() error { + cfg := DefaultConfig() + db, err := Connect(cfg) + if err != nil { + return err + } + DB = db + return nil +} + +// Close 关闭数据库连接 +func Close() error { + if DB != nil { + return DB.Close() + } + return nil +} + +// getEnv 获取环境变量,带默认值 +func getEnv(key, defaultVal string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultVal +} + +// getEnvInt 获取整数类型环境变量 +func getEnvInt(key string, defaultVal int) int { + if val := os.Getenv(key); val != "" { + var i int + if _, err := fmt.Sscanf(val, "%d", &i); err == nil { + return i + } + } + return defaultVal +} diff --git a/internal/db/migrations.go b/internal/db/migrations.go new file mode 100644 index 0000000..ae2c7ec --- /dev/null +++ b/internal/db/migrations.go @@ -0,0 +1,204 @@ +package db + +import ( + "database/sql" + "fmt" + "log" + "os" + "time" + + "gpt-manager-go/internal/auth" +) + +// Migrate 执行数据库迁移 +func Migrate(db *sql.DB) error { + log.Println("Starting database migration...") + + // 创建表的 SQL 语句 + migrations := []string{ + createAdminsTable, + createChatGPTAccountsTable, + createCardKeysTable, + createInvitationsTable, + createAPIKeysTable, + createSystemSettingsTable, + insertDefaultSettings, + } + + for i, migration := range migrations { + if _, err := db.Exec(migration); err != nil { + return fmt.Errorf("migration %d failed: %w", i+1, err) + } + } + + // 创建默认管理员 + if err := CreateDefaultAdmin(db); err != nil { + log.Printf("Warning: Failed to create default admin: %v", err) + } + + log.Println("Database migration completed successfully") + return nil +} + +// CreateDefaultAdmin 创建默认管理员(如果不存在) +func CreateDefaultAdmin(db *sql.DB) error { + username := os.Getenv("ADMIN_USERNAME") + email := os.Getenv("ADMIN_EMAIL") + password := os.Getenv("ADMIN_PASSWORD") + + // 如果没有配置管理员信息,跳过 + if username == "" || email == "" || password == "" { + log.Println("No default admin configuration found, skipping...") + return nil + } + + // 检查是否已存在管理员 + var count int + err := db.QueryRow("SELECT COUNT(*) FROM admins").Scan(&count) + if err != nil { + return fmt.Errorf("failed to check admins: %w", err) + } + + if count > 0 { + log.Println("Admin already exists, skipping default admin creation...") + return nil + } + + // 创建密码哈希 + passwordHash, err := auth.HashPassword(password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + // 创建管理员 + _, err = db.Exec(` + INSERT INTO admins (username, email, password_hash, is_super_admin, is_active, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + `, username, email, passwordHash, true, true, time.Now()) + + if err != nil { + return fmt.Errorf("failed to create admin: %w", err) + } + + log.Printf("Default admin '%s' created successfully", username) + return nil +} + +const createAdminsTable = ` +CREATE TABLE IF NOT EXISTS admins ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(120) NOT NULL UNIQUE, + password_hash VARCHAR(128) NOT NULL, + is_super_admin BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_admins_username ON admins(username); +CREATE INDEX IF NOT EXISTS idx_admins_email ON admins(email); +` + +const createChatGPTAccountsTable = ` +CREATE TABLE IF NOT EXISTS chatgpt_accounts ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + auth_token TEXT NOT NULL, + team_account_id VARCHAR(100) NOT NULL UNIQUE, + seats_in_use INTEGER NOT NULL DEFAULT 0, + seats_entitled INTEGER NOT NULL DEFAULT 0, + active_start TIMESTAMP, + active_until TIMESTAMP, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + consecutive_failures INTEGER NOT NULL DEFAULT 0, + last_check TIMESTAMP, + last_used TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_chatgpt_accounts_is_active ON chatgpt_accounts(is_active); +CREATE INDEX IF NOT EXISTS idx_chatgpt_accounts_team_account_id ON chatgpt_accounts(team_account_id); +` + +const createCardKeysTable = ` +CREATE TABLE IF NOT EXISTS card_keys ( + id SERIAL PRIMARY KEY, + key VARCHAR(19) NOT NULL UNIQUE, + max_uses INTEGER NOT NULL DEFAULT 1, + used_count INTEGER NOT NULL DEFAULT 0, + validity_type VARCHAR(20) NOT NULL, + expires_at TIMESTAMP NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by_id INTEGER NOT NULL REFERENCES admins(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_card_keys_key ON card_keys(key); +CREATE INDEX IF NOT EXISTS idx_card_keys_expires_at ON card_keys(expires_at); +CREATE INDEX IF NOT EXISTS idx_card_keys_is_active ON card_keys(is_active); +` + +const createInvitationsTable = ` +CREATE TABLE IF NOT EXISTS invitations ( + id SERIAL PRIMARY KEY, + card_key_id INTEGER REFERENCES card_keys(id), + account_id INTEGER NOT NULL REFERENCES chatgpt_accounts(id), + invited_email VARCHAR(120) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + error_message TEXT, + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_invitations_card_key_id ON invitations(card_key_id); +CREATE INDEX IF NOT EXISTS idx_invitations_account_id ON invitations(account_id); +CREATE INDEX IF NOT EXISTS idx_invitations_invited_email ON invitations(invited_email); +CREATE INDEX IF NOT EXISTS idx_invitations_status ON invitations(status); +CREATE INDEX IF NOT EXISTS idx_invitations_created_at ON invitations(created_at); +` + +const createAPIKeysTable = ` +CREATE TABLE IF NOT EXISTS api_keys ( + id SERIAL PRIMARY KEY, + key VARCHAR(64) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + created_by_id INTEGER NOT NULL REFERENCES admins(id), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + rate_limit INTEGER NOT NULL DEFAULT 60, + allowed_ips TEXT DEFAULT '[]', + last_used TIMESTAMP, + request_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key); +CREATE INDEX IF NOT EXISTS idx_api_keys_is_active ON api_keys(is_active); +` + +const createSystemSettingsTable = ` +CREATE TABLE IF NOT EXISTS system_settings ( + id SERIAL PRIMARY KEY, + key VARCHAR(100) NOT NULL UNIQUE, + value TEXT NOT NULL, + value_type VARCHAR(20) NOT NULL DEFAULT 'string', + description VARCHAR(255), + updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_system_settings_key ON system_settings(key); +` + +const insertDefaultSettings = ` +INSERT INTO system_settings (key, value, value_type, description) VALUES + ('turnstile_enabled', 'false', 'bool', 'Cloudflare Turnstile 开关'), + ('turnstile_site_key', '', 'string', 'Turnstile Site Key'), + ('turnstile_secret_key', '', 'string', 'Turnstile Secret Key'), + ('token_check_interval', '6', 'int', 'Token 检测间隔(小时)'), + ('token_failure_threshold', '2', 'int', '连续失败禁用阈值'), + ('invitation_validity_days', '30', 'int', '邀请有效期(天)'), + ('site_title', 'ChatGPT Team 邀请', 'string', '站点标题') +ON CONFLICT (key) DO NOTHING; +` diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go new file mode 100644 index 0000000..17efe04 --- /dev/null +++ b/internal/handler/auth_handler.go @@ -0,0 +1,138 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "gpt-manager-go/internal/auth" + "gpt-manager-go/internal/repository" +) + +// AuthHandler 认证处理器 +type AuthHandler struct { + adminRepo *repository.AdminRepository +} + +// NewAuthHandler 创建认证处理器 +func NewAuthHandler(adminRepo *repository.AdminRepository) *AuthHandler { + return &AuthHandler{adminRepo: adminRepo} +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Token string `json:"token,omitempty"` + User *UserInfo `json:"user,omitempty"` +} + +// UserInfo 用户信息 +type UserInfo struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + IsSuperAdmin bool `json:"is_super_admin"` +} + +// Login 登录接口 +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, LoginResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, LoginResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证必填字段 + if req.Username == "" || req.Password == "" { + respondJSON(w, http.StatusBadRequest, LoginResponse{ + Success: false, + Message: "Username and password are required", + }) + return + } + + // 查找用户 + admin, err := h.adminRepo.FindByUsername(req.Username) + if err != nil { + respondJSON(w, http.StatusInternalServerError, LoginResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + + if admin == nil { + respondJSON(w, http.StatusUnauthorized, LoginResponse{ + Success: false, + Message: "Invalid username or password", + }) + return + } + + // 检查账号是否激活 + if !admin.IsActive { + respondJSON(w, http.StatusForbidden, LoginResponse{ + Success: false, + Message: "Account is disabled", + }) + return + } + + // 验证密码 + if !auth.CheckPassword(req.Password, admin.PasswordHash) { + respondJSON(w, http.StatusUnauthorized, LoginResponse{ + Success: false, + Message: "Invalid username or password", + }) + return + } + + // 生成 Token + token, err := auth.GenerateToken(admin.ID, admin.Username, admin.IsSuperAdmin) + if err != nil { + respondJSON(w, http.StatusInternalServerError, LoginResponse{ + Success: false, + Message: "Failed to generate token", + }) + return + } + + // 更新最后登录时间 + _ = h.adminRepo.UpdateLastLogin(admin.ID) + + respondJSON(w, http.StatusOK, LoginResponse{ + Success: true, + Message: "Login successful", + Token: token, + User: &UserInfo{ + ID: admin.ID, + Username: admin.Username, + Email: admin.Email, + IsSuperAdmin: admin.IsSuperAdmin, + }, + }) +} + +// respondJSON 返回 JSON 响应 +func respondJSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(data) +} diff --git a/internal/handler/card_key_handler.go b/internal/handler/card_key_handler.go new file mode 100644 index 0000000..aa2115c --- /dev/null +++ b/internal/handler/card_key_handler.go @@ -0,0 +1,213 @@ +package handler + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "net/http" + "strings" + "time" + + "gpt-manager-go/internal/middleware" + "gpt-manager-go/internal/models" + "gpt-manager-go/internal/repository" +) + +// CardKeyHandler 卡密处理器 +type CardKeyHandler struct { + repo *repository.CardKeyRepository +} + +// NewCardKeyHandler 创建处理器 +func NewCardKeyHandler(repo *repository.CardKeyRepository) *CardKeyHandler { + return &CardKeyHandler{repo: repo} +} + +// CreateCardKeyRequest 创建卡密请求 +type CreateCardKeyRequest struct { + ValidityDays int `json:"validity_days"` // 有效期天数,默认30 + MaxUses int `json:"max_uses"` // 最大使用次数,默认1 +} + +// BatchCreateCardKeyRequest 批量创建卡密请求 +type BatchCreateCardKeyRequest struct { + Count int `json:"count"` // 创建数量 + ValidityDays int `json:"validity_days"` // 有效期天数,默认30 + MaxUses int `json:"max_uses"` // 最大使用次数,默认1 +} + +// CardKeyResponse 卡密响应 +type CardKeyResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data *models.CardKey `json:"data,omitempty"` + Keys []*models.CardKey `json:"keys,omitempty"` +} + +// Handle 处理卡密接口 (GET: 列表, POST: 创建) +func (h *CardKeyHandler) Handle(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.list(w, r) + case http.MethodPost: + h.create(w, r) + default: + respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{ + Success: false, + Message: "Method not allowed", + }) + } +} + +// create 创建单个卡密 +func (h *CardKeyHandler) create(w http.ResponseWriter, r *http.Request) { + + var req CreateCardKeyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + // 允许空请求体,使用默认值 + req = CreateCardKeyRequest{} + } + + // 设置默认值 + if req.ValidityDays <= 0 { + req.ValidityDays = 30 + } + if req.MaxUses <= 0 { + req.MaxUses = 1 + } + + // 获取当前用户 + claims := middleware.GetUserFromContext(r.Context()) + + // 生成卡密 + cardKey := &models.CardKey{ + Key: generateCardKey(), + MaxUses: req.MaxUses, + UsedCount: 0, + ValidityType: "custom", + ExpiresAt: time.Now().AddDate(0, 0, req.ValidityDays), + IsActive: true, + CreatedByID: claims.UserID, + CreatedAt: time.Now(), + } + + if err := h.repo.Create(cardKey); err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to create card key: " + err.Error(), + }) + return + } + + respondJSON(w, http.StatusCreated, CardKeyResponse{ + Success: true, + Message: "Card key created successfully", + Data: cardKey, + }) +} + +// BatchCreate 批量创建卡密 (POST /api/cardkeys/batch) +func (h *CardKeyHandler) BatchCreate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req BatchCreateCardKeyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证和设置默认值 + if req.Count <= 0 { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Count must be greater than 0", + }) + return + } + if req.Count > 100 { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Count cannot exceed 100", + }) + return + } + if req.ValidityDays <= 0 { + req.ValidityDays = 30 + } + if req.MaxUses <= 0 { + req.MaxUses = 1 + } + + // 获取当前用户 + claims := middleware.GetUserFromContext(r.Context()) + + // 批量创建 + var createdKeys []*models.CardKey + expiresAt := time.Now().AddDate(0, 0, req.ValidityDays) + + for i := 0; i < req.Count; i++ { + cardKey := &models.CardKey{ + Key: generateCardKey(), + MaxUses: req.MaxUses, + UsedCount: 0, + ValidityType: "custom", + ExpiresAt: expiresAt, + IsActive: true, + CreatedByID: claims.UserID, + CreatedAt: time.Now(), + } + + if err := h.repo.Create(cardKey); err != nil { + // 如果创建失败,返回已创建的卡密 + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to create some card keys", + Keys: createdKeys, + }) + return + } + createdKeys = append(createdKeys, cardKey) + } + + respondJSON(w, http.StatusCreated, CardKeyResponse{ + Success: true, + Message: "Card keys created successfully", + Keys: createdKeys, + }) +} + +// list 获取卡密列表 +func (h *CardKeyHandler) list(w http.ResponseWriter, r *http.Request) { + + cardKeys, err := h.repo.FindAll() + if err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to fetch card keys", + }) + return + } + + respondJSON(w, http.StatusOK, CardKeyResponse{ + Success: true, + Message: "OK", + Keys: cardKeys, + }) +} + +// generateCardKey 生成卡密格式: XXXX-XXXX-XXXX-XXXX +func generateCardKey() string { + bytes := make([]byte, 8) + rand.Read(bytes) + hex := strings.ToUpper(hex.EncodeToString(bytes)) + return hex[0:4] + "-" + hex[4:8] + "-" + hex[8:12] + "-" + hex[12:16] +} diff --git a/internal/handler/chatgpt_account_handler.go b/internal/handler/chatgpt_account_handler.go new file mode 100644 index 0000000..aad656b --- /dev/null +++ b/internal/handler/chatgpt_account_handler.go @@ -0,0 +1,287 @@ +package handler + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + "time" + + "gpt-manager-go/internal/models" + "gpt-manager-go/internal/repository" + "gpt-manager-go/internal/service" +) + +// ChatGPTAccountHandler ChatGPT 账号处理器 +type ChatGPTAccountHandler struct { + repo *repository.ChatGPTAccountRepository + chatgptService *service.ChatGPTService +} + +// NewChatGPTAccountHandler 创建处理器 +func NewChatGPTAccountHandler(repo *repository.ChatGPTAccountRepository, chatgptService *service.ChatGPTService) *ChatGPTAccountHandler { + return &ChatGPTAccountHandler{ + repo: repo, + chatgptService: chatgptService, + } +} + +// CreateAccountRequest 创建账号请求 +type CreateAccountRequest struct { + Name string `json:"name"` + TeamAccountID string `json:"team_account_id"` + AuthToken string `json:"auth_token"` +} + +// AccountResponse 账号响应 +type AccountResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data *models.ChatGPTAccount `json:"data,omitempty"` +} + +// ListResponse 列表响应 +type ListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data []*models.ChatGPTAccount `json:"data,omitempty"` +} + +// Create 创建账号 +func (h *ChatGPTAccountHandler) Create(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req CreateAccountRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, AccountResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证必填字段 + if req.TeamAccountID == "" || req.AuthToken == "" { + respondJSON(w, http.StatusBadRequest, AccountResponse{ + Success: false, + Message: "team_account_id and auth_token are required", + }) + return + } + + // 检查是否已存在 + existing, err := h.repo.FindByTeamAccountID(req.TeamAccountID) + if err != nil { + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Database error: " + err.Error(), + }) + return + } + if existing != nil { + respondJSON(w, http.StatusConflict, AccountResponse{ + Success: false, + Message: "Team account already exists", + }) + return + } + + // 调用 ChatGPT API 获取订阅信息 + subInfo, err := h.chatgptService.GetSubscription(req.TeamAccountID, req.AuthToken) + if err != nil { + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Failed to verify team account: " + err.Error(), + }) + return + } + + // 设置账号名称 + name := req.Name + if name == "" { + name = "Team-" + req.TeamAccountID[:8] + } + + // 创建账号 + account := &models.ChatGPTAccount{ + Name: name, + AuthToken: req.AuthToken, + TeamAccountID: req.TeamAccountID, + SeatsInUse: subInfo.SeatsInUse, + SeatsEntitled: subInfo.SeatsEntitled, + IsActive: subInfo.IsValid, + } + + // 设置时间 + if subInfo.IsValid { + account.ActiveStart = sql.NullTime{Time: subInfo.ActiveStart, Valid: !subInfo.ActiveStart.IsZero()} + account.ActiveUntil = sql.NullTime{Time: subInfo.ActiveUntil, Valid: !subInfo.ActiveUntil.IsZero()} + account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true} + } + + if err := h.repo.Create(account); err != nil { + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Failed to create account: " + err.Error(), + }) + return + } + + // 隐藏敏感信息 + account.AuthToken = "" + + respondJSON(w, http.StatusCreated, AccountResponse{ + Success: true, + Message: "Account created successfully", + Data: account, + }) +} + +// List 获取账号列表 +func (h *ChatGPTAccountHandler) List(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + respondJSON(w, http.StatusMethodNotAllowed, ListResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + accounts, err := h.repo.FindAll() + if err != nil { + respondJSON(w, http.StatusInternalServerError, ListResponse{ + Success: false, + Message: "Failed to fetch accounts", + }) + return + } + + // 隐藏敏感信息 + for _, a := range accounts { + a.AuthToken = "" + } + + respondJSON(w, http.StatusOK, ListResponse{ + Success: true, + Message: "OK", + Data: accounts, + }) +} + +// Refresh 刷新账号信息 +func (h *ChatGPTAccountHandler) Refresh(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + // 获取账号 ID + idStr := r.URL.Query().Get("id") + id, err := strconv.Atoi(idStr) + if err != nil { + respondJSON(w, http.StatusBadRequest, AccountResponse{ + Success: false, + Message: "Invalid account ID", + }) + return + } + + account, err := h.repo.FindByID(id) + if err != nil || account == nil { + respondJSON(w, http.StatusNotFound, AccountResponse{ + Success: false, + Message: "Account not found", + }) + return + } + + // 调用 ChatGPT API 获取订阅信息 + subInfo, err := h.chatgptService.GetSubscription(account.TeamAccountID, account.AuthToken) + if err != nil { + // API 调用失败,增加失败计数并设为不活跃 + account.ConsecutiveFailures++ + account.IsActive = false + account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true} + h.repo.Update(account) + + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Failed to refresh: " + err.Error(), + }) + return + } + + // 更新账号信息 + if subInfo.IsValid { + account.SeatsInUse = subInfo.SeatsInUse + account.SeatsEntitled = subInfo.SeatsEntitled + account.ActiveStart = sql.NullTime{Time: subInfo.ActiveStart, Valid: !subInfo.ActiveStart.IsZero()} + account.ActiveUntil = sql.NullTime{Time: subInfo.ActiveUntil, Valid: !subInfo.ActiveUntil.IsZero()} + account.IsActive = true + account.ConsecutiveFailures = 0 + } else { + account.IsActive = false + account.ConsecutiveFailures++ + } + account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true} + + if err := h.repo.Update(account); err != nil { + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Failed to update account", + }) + return + } + + // 隐藏敏感信息 + account.AuthToken = "" + + respondJSON(w, http.StatusOK, AccountResponse{ + Success: true, + Message: "Account refreshed successfully", + Data: account, + }) +} + +// Delete 删除账号 +func (h *ChatGPTAccountHandler) Delete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + idStr := r.URL.Query().Get("id") + id, err := strconv.Atoi(idStr) + if err != nil { + respondJSON(w, http.StatusBadRequest, AccountResponse{ + Success: false, + Message: "Invalid account ID", + }) + return + } + + if err := h.repo.Delete(id); err != nil { + respondJSON(w, http.StatusInternalServerError, AccountResponse{ + Success: false, + Message: "Failed to delete account", + }) + return + } + + respondJSON(w, http.StatusOK, AccountResponse{ + Success: true, + Message: "Account deleted successfully", + }) +} diff --git a/internal/handler/invite_handler.go b/internal/handler/invite_handler.go new file mode 100644 index 0000000..10dd6a2 --- /dev/null +++ b/internal/handler/invite_handler.go @@ -0,0 +1,393 @@ +package handler + +import ( + "database/sql" + "encoding/json" + "net/http" + "time" + + "gpt-manager-go/internal/models" + "gpt-manager-go/internal/repository" + "gpt-manager-go/internal/service" +) + +// InviteHandler 邀请处理器 +type InviteHandler struct { + accountRepo *repository.ChatGPTAccountRepository + invitationRepo *repository.InvitationRepository + cardKeyRepo *repository.CardKeyRepository + chatgptService *service.ChatGPTService +} + +// NewInviteHandler 创建邀请处理器 +func NewInviteHandler( + accountRepo *repository.ChatGPTAccountRepository, + invitationRepo *repository.InvitationRepository, + cardKeyRepo *repository.CardKeyRepository, + chatgptService *service.ChatGPTService, +) *InviteHandler { + return &InviteHandler{ + accountRepo: accountRepo, + invitationRepo: invitationRepo, + cardKeyRepo: cardKeyRepo, + chatgptService: chatgptService, + } +} + +// AdminInviteRequest 管理员邀请请求 +type AdminInviteRequest struct { + Email string `json:"email"` + AccountID int `json:"account_id"` // 可选,不传则自动选择 +} + +// CardKeyInviteRequest 卡密邀请请求 +type CardKeyInviteRequest struct { + CardKey string `json:"card_key"` + Email string `json:"email"` +} + +// InviteResponse 邀请响应 +type InviteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + InvitationID int `json:"invitation_id,omitempty"` + AccountName string `json:"account_name,omitempty"` +} + +// InviteByAdmin 管理员邀请/移除接口 (POST/DELETE /api/invite) +func (h *InviteHandler) InviteByAdmin(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + h.handleInvite(w, r) + case http.MethodDelete: + h.handleRemoveMember(w, r) + default: + respondJSON(w, http.StatusMethodNotAllowed, InviteResponse{ + Success: false, + Message: "Method not allowed", + }) + } +} + +// handleInvite 处理邀请逻辑 +func (h *InviteHandler) handleInvite(w http.ResponseWriter, r *http.Request) { + + var req AdminInviteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证邮箱 + if req.Email == "" { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Email is required", + }) + return + } + + // 获取账号 + var account *models.ChatGPTAccount + var err error + + if req.AccountID > 0 { + // 指定账号 + account, err = h.accountRepo.FindByID(req.AccountID) + if err != nil || account == nil { + respondJSON(w, http.StatusNotFound, InviteResponse{ + Success: false, + Message: "Account not found", + }) + return + } + if !account.IsActive || account.AvailableSeats() <= 0 { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Account is not active or has no available seats", + }) + return + } + } else { + // 自动选择可用账号 + account, err = h.findAvailableAccount() + if err != nil || account == nil { + respondJSON(w, http.StatusServiceUnavailable, InviteResponse{ + Success: false, + Message: "No available team accounts", + }) + return + } + } + + // 发送邀请 + inviteResp, err := h.chatgptService.SendInvite(account.TeamAccountID, account.AuthToken, req.Email) + if err != nil { + respondJSON(w, http.StatusInternalServerError, InviteResponse{ + Success: false, + Message: "Failed to send invite: " + err.Error(), + }) + return + } + + // 创建邀请记录 + invitation := &models.Invitation{ + AccountID: account.ID, + InvitedEmail: req.Email, + Status: models.StatusSent, + CreatedAt: time.Now(), + } + + if inviteResp.Success { + invitation.Status = models.StatusSent + } else { + invitation.Status = models.StatusFailed + invitation.ErrorMessage = sql.NullString{String: inviteResp.Message, Valid: true} + } + + if err := h.invitationRepo.Create(invitation); err != nil { + // 记录失败不影响主流程 + } + + if !inviteResp.Success { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: inviteResp.Message, + }) + return + } + + // 更新账号使用时间 + h.accountRepo.UpdateLastUsed(account.ID) + + respondJSON(w, http.StatusOK, InviteResponse{ + Success: true, + Message: "Invitation sent successfully", + InvitationID: invitation.ID, + AccountName: account.Name, + }) +} + +// InviteByCardKey 卡密邀请 (POST /api/invite/card) +func (h *InviteHandler) InviteByCardKey(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, InviteResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req CardKeyInviteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证必填字段 + if req.CardKey == "" || req.Email == "" { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Card key and email are required", + }) + return + } + + // 验证卡密 + cardKey, err := h.cardKeyRepo.FindByKey(req.CardKey) + if err != nil { + respondJSON(w, http.StatusInternalServerError, InviteResponse{ + Success: false, + Message: "Database error", + }) + return + } + if cardKey == nil { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Invalid card key", + }) + return + } + + // 检查卡密是否可用 + if !cardKey.IsUsable() { + if cardKey.IsExpired() { + respondJSON(w, http.StatusForbidden, InviteResponse{ + Success: false, + Message: "Card key has expired", + }) + } else if cardKey.UsedCount >= cardKey.MaxUses { + respondJSON(w, http.StatusForbidden, InviteResponse{ + Success: false, + Message: "Card key usage limit exceeded", + }) + } else { + respondJSON(w, http.StatusForbidden, InviteResponse{ + Success: false, + Message: "Card key is not active", + }) + } + return + } + + // 查找可用账号 + account, err := h.findAvailableAccount() + if err != nil || account == nil { + respondJSON(w, http.StatusServiceUnavailable, InviteResponse{ + Success: false, + Message: "No available team accounts", + }) + return + } + + // 发送邀请 + inviteResp, err := h.chatgptService.SendInvite(account.TeamAccountID, account.AuthToken, req.Email) + if err != nil { + respondJSON(w, http.StatusInternalServerError, InviteResponse{ + Success: false, + Message: "Failed to send invite: " + err.Error(), + }) + return + } + + // 创建邀请记录 + invitation := &models.Invitation{ + CardKeyID: sql.NullInt64{Int64: int64(cardKey.ID), Valid: true}, + AccountID: account.ID, + InvitedEmail: req.Email, + CreatedAt: time.Now(), + } + + if inviteResp.Success { + invitation.Status = models.StatusSent + } else { + invitation.Status = models.StatusFailed + invitation.ErrorMessage = sql.NullString{String: inviteResp.Message, Valid: true} + } + + if err := h.invitationRepo.Create(invitation); err != nil { + // 记录失败不影响主流程 + } + + if !inviteResp.Success { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: inviteResp.Message, + }) + return + } + + // 增加卡密使用次数 + h.cardKeyRepo.IncrementUsedCount(cardKey.ID) + + // 更新账号使用时间 + h.accountRepo.UpdateLastUsed(account.ID) + + respondJSON(w, http.StatusOK, InviteResponse{ + Success: true, + Message: "Invitation sent successfully", + }) +} + +// findAvailableAccount 查找可用账号(轮询) +func (h *InviteHandler) findAvailableAccount() (*models.ChatGPTAccount, error) { + accounts, err := h.accountRepo.FindActiveWithAvailableSeats() + if err != nil { + return nil, err + } + if len(accounts) == 0 { + return nil, nil + } + // 返回第一个(已按可用席位数降序、最后使用时间升序排序) + return accounts[0], nil +} + +// RemoveMemberRequest 移除成员请求 +type RemoveMemberRequest struct { + Email string `json:"email"` + AccountID int `json:"account_id"` // 必填,指定从哪个账号移除 +} + +// handleRemoveMember 处理移除成员逻辑 +func (h *InviteHandler) handleRemoveMember(w http.ResponseWriter, r *http.Request) { + + var req RemoveMemberRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + // 验证必填字段 + if req.Email == "" { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Email is required", + }) + return + } + + if req.AccountID <= 0 { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Account ID is required", + }) + return + } + + // 获取账号 + account, err := h.accountRepo.FindByID(req.AccountID) + if err != nil || account == nil { + respondJSON(w, http.StatusNotFound, InviteResponse{ + Success: false, + Message: "Account not found", + }) + return + } + + if !account.IsActive { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: "Account is not active", + }) + return + } + + // 调用 ChatGPT API 移除成员 + removeResp, err := h.chatgptService.RemoveMember(account.TeamAccountID, account.AuthToken, req.Email) + if err != nil { + respondJSON(w, http.StatusInternalServerError, InviteResponse{ + Success: false, + Message: "Failed to remove member: " + err.Error(), + }) + return + } + + if !removeResp.Success { + respondJSON(w, http.StatusBadRequest, InviteResponse{ + Success: false, + Message: removeResp.Message, + }) + return + } + + // 删除邀请记录 + if err := h.invitationRepo.DeleteByEmailAndAccountID(req.Email, req.AccountID); err != nil { + // 删除记录失败不影响主流程,只记录日志 + } + + respondJSON(w, http.StatusOK, InviteResponse{ + Success: true, + Message: "Member removed successfully", + AccountName: account.Name, + }) +} diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..0ff76f1 --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,65 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "gpt-manager-go/internal/auth" +) + +// 上下文键类型 +type contextKey string + +const UserContextKey contextKey = "user" + +// AuthMiddleware JWT 认证中间件 +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, `{"success":false,"message":"Authorization header required"}`, http.StatusUnauthorized) + return + } + + // 检查 Bearer 前缀 + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, `{"success":false,"message":"Invalid authorization header format"}`, http.StatusUnauthorized) + return + } + + tokenString := parts[1] + + // 解析 Token + claims, err := auth.ParseToken(tokenString) + if err != nil { + http.Error(w, `{"success":false,"message":"Invalid or expired token"}`, http.StatusUnauthorized) + return + } + + // 将用户信息存入上下文 + ctx := context.WithValue(r.Context(), UserContextKey, claims) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// GetUserFromContext 从上下文获取用户信息 +func GetUserFromContext(ctx context.Context) *auth.Claims { + if claims, ok := ctx.Value(UserContextKey).(*auth.Claims); ok { + return claims + } + return nil +} + +// RequireSuperAdmin 要求超级管理员权限 +func RequireSuperAdmin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims := GetUserFromContext(r.Context()) + if claims == nil || !claims.IsSuperAdmin { + http.Error(w, `{"success":false,"message":"Super admin required"}`, http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/internal/models/admin.go b/internal/models/admin.go new file mode 100644 index 0000000..7370a09 --- /dev/null +++ b/internal/models/admin.go @@ -0,0 +1,23 @@ +package models + +import ( + "database/sql" + "time" +) + +// Admin 管理员表 +type Admin struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + PasswordHash string `json:"-"` // 密码哈希不输出到 JSON + IsSuperAdmin bool `json:"is_super_admin"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + LastLogin sql.NullTime `json:"last_login"` +} + +// TableName 返回表名 +func (Admin) TableName() string { + return "admins" +} diff --git a/internal/models/api_key.go b/internal/models/api_key.go new file mode 100644 index 0000000..bc8735e --- /dev/null +++ b/internal/models/api_key.go @@ -0,0 +1,25 @@ +package models + +import ( + "database/sql" + "time" +) + +// APIKey API 密钥表 +type APIKey struct { + ID int `json:"id"` + Key string `json:"key"` // 格式: sk_live_xxx + Name string `json:"name"` + CreatedByID int `json:"created_by_id"` + IsActive bool `json:"is_active"` + RateLimit int `json:"rate_limit"` // 次/分钟 + AllowedIPs string `json:"allowed_ips"` // JSON 数组 + LastUsed sql.NullTime `json:"last_used"` + RequestCount int `json:"request_count"` + CreatedAt time.Time `json:"created_at"` +} + +// TableName 返回表名 +func (APIKey) TableName() string { + return "api_keys" +} diff --git a/internal/models/card_key.go b/internal/models/card_key.go new file mode 100644 index 0000000..f3eb978 --- /dev/null +++ b/internal/models/card_key.go @@ -0,0 +1,42 @@ +package models + +import ( + "time" +) + +// CardKey 卡密表 +type CardKey struct { + ID int `json:"id"` + Key string `json:"key"` // 格式: XXXX-XXXX-XXXX-XXXX + MaxUses int `json:"max_uses"` + UsedCount int `json:"used_count"` + ValidityType string `json:"validity_type"` // month/quarter/year/custom + ExpiresAt time.Time `json:"expires_at"` + IsActive bool `json:"is_active"` + CreatedByID int `json:"created_by_id"` + CreatedAt time.Time `json:"created_at"` +} + +// TableName 返回表名 +func (CardKey) TableName() string { + return "card_keys" +} + +// IsExpired 检查卡密是否过期 +func (c *CardKey) IsExpired() bool { + return time.Now().After(c.ExpiresAt) +} + +// IsUsable 检查卡密是否可用 +func (c *CardKey) IsUsable() bool { + return c.IsActive && !c.IsExpired() && c.UsedCount < c.MaxUses +} + +// RemainingUses 返回剩余使用次数 +func (c *CardKey) RemainingUses() int { + remaining := c.MaxUses - c.UsedCount + if remaining < 0 { + return 0 + } + return remaining +} diff --git a/internal/models/chatgpt_account.go b/internal/models/chatgpt_account.go new file mode 100644 index 0000000..9838d4c --- /dev/null +++ b/internal/models/chatgpt_account.go @@ -0,0 +1,109 @@ +package models + +import ( + "database/sql" + "encoding/json" + "time" +) + +// ChatGPTAccount ChatGPT Team 账号表 +type ChatGPTAccount struct { + ID int `json:"id"` + Name string `json:"name"` + AuthToken string `json:"-"` // Token 不输出到 JSON + TeamAccountID string `json:"team_account_id"` + SeatsInUse int `json:"seats_in_use"` + SeatsEntitled int `json:"seats_entitled"` + ActiveStart sql.NullTime `json:"-"` + ActiveUntil sql.NullTime `json:"-"` + IsActive bool `json:"is_active"` + ConsecutiveFailures int `json:"consecutive_failures"` + LastCheck sql.NullTime `json:"-"` + LastUsed sql.NullTime `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt sql.NullTime `json:"-"` +} + +// ChatGPTAccountJSON 用于 JSON 序列化的结构 +type ChatGPTAccountJSON struct { + ID int `json:"id"` + Name string `json:"name"` + TeamAccountID string `json:"team_account_id"` + SeatsInUse int `json:"seats_in_use"` + SeatsEntitled int `json:"seats_entitled"` + ActiveStart *string `json:"active_start"` + ActiveUntil *string `json:"active_until"` + IsActive bool `json:"is_active"` + ConsecutiveFailures int `json:"consecutive_failures"` + LastCheck *string `json:"last_check"` + LastUsed *string `json:"last_used"` + CreatedAt string `json:"created_at"` + UpdatedAt *string `json:"updated_at"` + AvailableSeats int `json:"available_seats"` + UsagePercentage float64 `json:"usage_percentage"` +} + +// MarshalJSON 自定义 JSON 序列化 +func (a ChatGPTAccount) MarshalJSON() ([]byte, error) { + j := ChatGPTAccountJSON{ + ID: a.ID, + Name: a.Name, + TeamAccountID: a.TeamAccountID, + SeatsInUse: a.SeatsInUse, + SeatsEntitled: a.SeatsEntitled, + IsActive: a.IsActive, + ConsecutiveFailures: a.ConsecutiveFailures, + CreatedAt: a.CreatedAt.Format(time.RFC3339), + AvailableSeats: a.AvailableSeats(), + UsagePercentage: a.UsagePercentage(), + } + + if a.ActiveStart.Valid { + s := a.ActiveStart.Time.Format(time.RFC3339) + j.ActiveStart = &s + } + if a.ActiveUntil.Valid { + s := a.ActiveUntil.Time.Format(time.RFC3339) + j.ActiveUntil = &s + } + if a.LastCheck.Valid { + s := a.LastCheck.Time.Format(time.RFC3339) + j.LastCheck = &s + } + if a.LastUsed.Valid { + s := a.LastUsed.Time.Format(time.RFC3339) + j.LastUsed = &s + } + if a.UpdatedAt.Valid { + s := a.UpdatedAt.Time.Format(time.RFC3339) + j.UpdatedAt = &s + } + + return json.Marshal(j) +} + +// TableName 返回表名 +func (ChatGPTAccount) TableName() string { + return "chatgpt_accounts" +} + +// AvailableSeats 返回可用席位数量 +func (a *ChatGPTAccount) AvailableSeats() int { + return a.SeatsEntitled - a.SeatsInUse +} + +// UsagePercentage 返回席位使用百分比 +func (a *ChatGPTAccount) UsagePercentage() float64 { + if a.SeatsEntitled == 0 { + return 0 + } + return float64(a.SeatsInUse) / float64(a.SeatsEntitled) * 100 +} + +// IsSubscriptionValid 检查订阅是否有效 +func (a *ChatGPTAccount) IsSubscriptionValid() bool { + if !a.ActiveUntil.Valid { + return false + } + return time.Now().Before(a.ActiveUntil.Time) +} diff --git a/internal/models/invitation.go b/internal/models/invitation.go new file mode 100644 index 0000000..a749d2e --- /dev/null +++ b/internal/models/invitation.go @@ -0,0 +1,43 @@ +package models + +import ( + "database/sql" + "time" +) + +// InvitationStatus 邀请状态枚举 +type InvitationStatus string + +const ( + StatusPending InvitationStatus = "pending" + StatusSent InvitationStatus = "sent" + StatusAccepted InvitationStatus = "accepted" + StatusFailed InvitationStatus = "failed" + StatusExpired InvitationStatus = "expired" +) + +// Invitation 邀请记录表 +type Invitation struct { + ID int `json:"id"` + CardKeyID sql.NullInt64 `json:"card_key_id"` + AccountID int `json:"account_id"` + InvitedEmail string `json:"invited_email"` + Status InvitationStatus `json:"status"` + ErrorMessage sql.NullString `json:"error_message"` + ExpiresAt sql.NullTime `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +// TableName 返回表名 +func (Invitation) TableName() string { + return "invitations" +} + +// IsExpired 检查邀请是否过期 +func (i *Invitation) IsExpired() bool { + if !i.ExpiresAt.Valid { + return false + } + return time.Now().After(i.ExpiresAt.Time) +} diff --git a/internal/models/system_setting.go b/internal/models/system_setting.go new file mode 100644 index 0000000..43363ff --- /dev/null +++ b/internal/models/system_setting.go @@ -0,0 +1,42 @@ +package models + +import ( + "database/sql" +) + +// ValueType 配置值类型枚举 +type ValueType string + +const ( + ValueTypeString ValueType = "string" + ValueTypeInt ValueType = "int" + ValueTypeFloat ValueType = "float" + ValueTypeBool ValueType = "bool" + ValueTypeJSON ValueType = "json" +) + +// SystemSetting 系统配置表 +type SystemSetting struct { + ID int `json:"id"` + Key string `json:"key"` + Value string `json:"value"` + ValueType ValueType `json:"value_type"` + Description sql.NullString `json:"description"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +// TableName 返回表名 +func (SystemSetting) TableName() string { + return "system_settings" +} + +// 默认配置键名常量 +const ( + SettingTurnstileEnabled = "turnstile_enabled" + SettingTurnstileSiteKey = "turnstile_site_key" + SettingTurnstileSecretKey = "turnstile_secret_key" + SettingTokenCheckInterval = "token_check_interval" + SettingTokenFailureThreshold = "token_failure_threshold" + SettingInvitationValidityDays = "invitation_validity_days" + SettingSiteTitle = "site_title" +) diff --git a/internal/repository/admin_repo.go b/internal/repository/admin_repo.go new file mode 100644 index 0000000..6dbcd75 --- /dev/null +++ b/internal/repository/admin_repo.go @@ -0,0 +1,120 @@ +package repository + +import ( + "database/sql" + "time" + + "gpt-manager-go/internal/models" +) + +// AdminRepository 管理员仓储 +type AdminRepository struct { + db *sql.DB +} + +// NewAdminRepository 创建管理员仓储 +func NewAdminRepository(db *sql.DB) *AdminRepository { + return &AdminRepository{db: db} +} + +// FindByUsername 根据用户名查找管理员 +func (r *AdminRepository) FindByUsername(username string) (*models.Admin, error) { + admin := &models.Admin{} + err := r.db.QueryRow(` + SELECT id, username, email, password_hash, is_super_admin, is_active, created_at, last_login + FROM admins WHERE username = $1 + `, username).Scan( + &admin.ID, + &admin.Username, + &admin.Email, + &admin.PasswordHash, + &admin.IsSuperAdmin, + &admin.IsActive, + &admin.CreatedAt, + &admin.LastLogin, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return admin, nil +} + +// FindByEmail 根据邮箱查找管理员 +func (r *AdminRepository) FindByEmail(email string) (*models.Admin, error) { + admin := &models.Admin{} + err := r.db.QueryRow(` + SELECT id, username, email, password_hash, is_super_admin, is_active, created_at, last_login + FROM admins WHERE email = $1 + `, email).Scan( + &admin.ID, + &admin.Username, + &admin.Email, + &admin.PasswordHash, + &admin.IsSuperAdmin, + &admin.IsActive, + &admin.CreatedAt, + &admin.LastLogin, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return admin, nil +} + +// FindByID 根据 ID 查找管理员 +func (r *AdminRepository) FindByID(id int) (*models.Admin, error) { + admin := &models.Admin{} + err := r.db.QueryRow(` + SELECT id, username, email, password_hash, is_super_admin, is_active, created_at, last_login + FROM admins WHERE id = $1 + `, id).Scan( + &admin.ID, + &admin.Username, + &admin.Email, + &admin.PasswordHash, + &admin.IsSuperAdmin, + &admin.IsActive, + &admin.CreatedAt, + &admin.LastLogin, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return admin, nil +} + +// Create 创建管理员 +func (r *AdminRepository) Create(admin *models.Admin) error { + return r.db.QueryRow(` + INSERT INTO admins (username, email, password_hash, is_super_admin, is_active, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id + `, + admin.Username, + admin.Email, + admin.PasswordHash, + admin.IsSuperAdmin, + admin.IsActive, + time.Now(), + ).Scan(&admin.ID) +} + +// UpdateLastLogin 更新最后登录时间 +func (r *AdminRepository) UpdateLastLogin(id int) error { + _, err := r.db.Exec(` + UPDATE admins SET last_login = $1 WHERE id = $2 + `, time.Now(), id) + return err +} diff --git a/internal/repository/card_key_repo.go b/internal/repository/card_key_repo.go new file mode 100644 index 0000000..332207d --- /dev/null +++ b/internal/repository/card_key_repo.go @@ -0,0 +1,89 @@ +package repository + +import ( + "database/sql" + + "gpt-manager-go/internal/models" +) + +// CardKeyRepository 卡密仓储 +type CardKeyRepository struct { + db *sql.DB +} + +// NewCardKeyRepository 创建仓储 +func NewCardKeyRepository(db *sql.DB) *CardKeyRepository { + return &CardKeyRepository{db: db} +} + +// FindByKey 根据卡密查找 +func (r *CardKeyRepository) FindByKey(key string) (*models.CardKey, error) { + cardKey := &models.CardKey{} + err := r.db.QueryRow(` + SELECT id, key, max_uses, used_count, validity_type, expires_at, is_active, created_by_id, created_at + FROM card_keys WHERE key = $1 + `, key).Scan( + &cardKey.ID, &cardKey.Key, &cardKey.MaxUses, &cardKey.UsedCount, + &cardKey.ValidityType, &cardKey.ExpiresAt, &cardKey.IsActive, + &cardKey.CreatedByID, &cardKey.CreatedAt, + ) + if err == sql.ErrNoRows { + return nil, nil + } + return cardKey, err +} + +// IncrementUsedCount 增加使用次数,如果达到最大次数则设为不活跃 +func (r *CardKeyRepository) IncrementUsedCount(id int) error { + _, err := r.db.Exec(` + UPDATE card_keys + SET used_count = used_count + 1, + is_active = CASE WHEN used_count + 1 >= max_uses THEN false ELSE is_active END + WHERE id = $1 + `, id) + return err +} + +// Create 创建卡密 +func (r *CardKeyRepository) Create(cardKey *models.CardKey) error { + return r.db.QueryRow(` + INSERT INTO card_keys (key, max_uses, used_count, validity_type, expires_at, is_active, created_by_id, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id + `, + cardKey.Key, + cardKey.MaxUses, + cardKey.UsedCount, + cardKey.ValidityType, + cardKey.ExpiresAt, + cardKey.IsActive, + cardKey.CreatedByID, + cardKey.CreatedAt, + ).Scan(&cardKey.ID) +} + +// FindAll 获取所有卡密 +func (r *CardKeyRepository) FindAll() ([]*models.CardKey, error) { + rows, err := r.db.Query(` + SELECT id, key, max_uses, used_count, validity_type, expires_at, is_active, created_by_id, created_at + FROM card_keys ORDER BY created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var cardKeys []*models.CardKey + for rows.Next() { + ck := &models.CardKey{} + if err := rows.Scan( + &ck.ID, &ck.Key, &ck.MaxUses, &ck.UsedCount, + &ck.ValidityType, &ck.ExpiresAt, &ck.IsActive, + &ck.CreatedByID, &ck.CreatedAt, + ); err != nil { + return nil, err + } + cardKeys = append(cardKeys, ck) + } + return cardKeys, nil +} diff --git a/internal/repository/chatgpt_account_repo.go b/internal/repository/chatgpt_account_repo.go new file mode 100644 index 0000000..80a1e79 --- /dev/null +++ b/internal/repository/chatgpt_account_repo.go @@ -0,0 +1,207 @@ +package repository + +import ( + "database/sql" + "time" + + "gpt-manager-go/internal/models" +) + +// ChatGPTAccountRepository ChatGPT 账号仓储 +type ChatGPTAccountRepository struct { + db *sql.DB +} + +// NewChatGPTAccountRepository 创建仓储 +func NewChatGPTAccountRepository(db *sql.DB) *ChatGPTAccountRepository { + return &ChatGPTAccountRepository{db: db} +} + +// Create 创建账号 +func (r *ChatGPTAccountRepository) Create(account *models.ChatGPTAccount) error { + return r.db.QueryRow(` + INSERT INTO chatgpt_accounts (name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING id + `, + account.Name, + account.AuthToken, + account.TeamAccountID, + account.SeatsInUse, + account.SeatsEntitled, + account.ActiveStart, + account.ActiveUntil, + account.IsActive, + account.ConsecutiveFailures, + time.Now(), + ).Scan(&account.ID) +} + +// Update 更新账号 +func (r *ChatGPTAccountRepository) Update(account *models.ChatGPTAccount) error { + _, err := r.db.Exec(` + UPDATE chatgpt_accounts SET + name = $1, auth_token = $2, seats_in_use = $3, seats_entitled = $4, + active_start = $5, active_until = $6, is_active = $7, + consecutive_failures = $8, last_check = $9, updated_at = $10 + WHERE id = $11 + `, + account.Name, + account.AuthToken, + account.SeatsInUse, + account.SeatsEntitled, + account.ActiveStart, + account.ActiveUntil, + account.IsActive, + account.ConsecutiveFailures, + account.LastCheck, + time.Now(), + account.ID, + ) + return err +} + +// FindByID 根据 ID 查找 +func (r *ChatGPTAccountRepository) FindByID(id int) (*models.ChatGPTAccount, error) { + account := &models.ChatGPTAccount{} + err := r.db.QueryRow(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts WHERE id = $1 + `, id).Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ) + if err == sql.ErrNoRows { + return nil, nil + } + return account, err +} + +// FindByTeamAccountID 根据 Team Account ID 查找 +func (r *ChatGPTAccountRepository) FindByTeamAccountID(teamAccountID string) (*models.ChatGPTAccount, error) { + account := &models.ChatGPTAccount{} + err := r.db.QueryRow(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts WHERE team_account_id = $1 + `, teamAccountID).Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ) + if err == sql.ErrNoRows { + return nil, nil + } + return account, err +} + +// FindAll 获取所有账号 +func (r *ChatGPTAccountRepository) FindAll() ([]*models.ChatGPTAccount, error) { + rows, err := r.db.Query(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts ORDER BY created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var accounts []*models.ChatGPTAccount + for rows.Next() { + account := &models.ChatGPTAccount{} + if err := rows.Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ); err != nil { + return nil, err + } + accounts = append(accounts, account) + } + return accounts, nil +} + +// FindActive 获取所有激活的账号 +func (r *ChatGPTAccountRepository) FindActive() ([]*models.ChatGPTAccount, error) { + rows, err := r.db.Query(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts WHERE is_active = true ORDER BY created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var accounts []*models.ChatGPTAccount + for rows.Next() { + account := &models.ChatGPTAccount{} + if err := rows.Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ); err != nil { + return nil, err + } + accounts = append(accounts, account) + } + return accounts, nil +} + +// Delete 删除账号 +func (r *ChatGPTAccountRepository) Delete(id int) error { + _, err := r.db.Exec(`DELETE FROM chatgpt_accounts WHERE id = $1`, id) + return err +} + +// FindActiveWithAvailableSeats 查找激活且有可用席位的账号 +func (r *ChatGPTAccountRepository) FindActiveWithAvailableSeats() ([]*models.ChatGPTAccount, error) { + rows, err := r.db.Query(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts + WHERE is_active = true AND seats_entitled > seats_in_use + ORDER BY (seats_entitled - seats_in_use) DESC, last_used ASC NULLS FIRST + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var accounts []*models.ChatGPTAccount + for rows.Next() { + account := &models.ChatGPTAccount{} + if err := rows.Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ); err != nil { + return nil, err + } + accounts = append(accounts, account) + } + return accounts, nil +} + +// UpdateLastUsed 更新账号最后使用时间和席位 +func (r *ChatGPTAccountRepository) UpdateLastUsed(id int) error { + _, err := r.db.Exec(` + UPDATE chatgpt_accounts SET last_used = $1, seats_in_use = seats_in_use + 1, updated_at = $1 + WHERE id = $2 + `, time.Now(), id) + return err +} diff --git a/internal/repository/invitation_repo.go b/internal/repository/invitation_repo.go new file mode 100644 index 0000000..6d863a0 --- /dev/null +++ b/internal/repository/invitation_repo.go @@ -0,0 +1,83 @@ +package repository + +import ( + "database/sql" + "time" + + "gpt-manager-go/internal/models" +) + +// InvitationRepository 邀请记录仓储 +type InvitationRepository struct { + db *sql.DB +} + +// NewInvitationRepository 创建仓储 +func NewInvitationRepository(db *sql.DB) *InvitationRepository { + return &InvitationRepository{db: db} +} + +// Create 创建邀请记录 +func (r *InvitationRepository) Create(invitation *models.Invitation) error { + return r.db.QueryRow(` + INSERT INTO invitations (card_key_id, account_id, invited_email, status, error_message, expires_at, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + `, + invitation.CardKeyID, + invitation.AccountID, + invitation.InvitedEmail, + invitation.Status, + invitation.ErrorMessage, + invitation.ExpiresAt, + time.Now(), + ).Scan(&invitation.ID) +} + +// FindByEmail 根据邮箱查找邀请记录 +func (r *InvitationRepository) FindByEmail(email string) ([]*models.Invitation, error) { + rows, err := r.db.Query(` + SELECT id, card_key_id, account_id, invited_email, status, error_message, expires_at, created_at, updated_at + FROM invitations WHERE invited_email = $1 ORDER BY created_at DESC + `, email) + if err != nil { + return nil, err + } + defer rows.Close() + + var invitations []*models.Invitation + for rows.Next() { + inv := &models.Invitation{} + if err := rows.Scan( + &inv.ID, &inv.CardKeyID, &inv.AccountID, &inv.InvitedEmail, + &inv.Status, &inv.ErrorMessage, &inv.ExpiresAt, + &inv.CreatedAt, &inv.UpdatedAt, + ); err != nil { + return nil, err + } + invitations = append(invitations, inv) + } + return invitations, nil +} + +// UpdateStatus 更新邀请状态 +func (r *InvitationRepository) UpdateStatus(id int, status models.InvitationStatus, errMsg string) error { + var errMsgSQL sql.NullString + if errMsg != "" { + errMsgSQL = sql.NullString{String: errMsg, Valid: true} + } + + _, err := r.db.Exec(` + UPDATE invitations SET status = $1, error_message = $2, updated_at = $3 + WHERE id = $4 + `, status, errMsgSQL, time.Now(), id) + return err +} + +// DeleteByEmailAndAccountID 根据邮箱和账号ID删除邀请记录 +func (r *InvitationRepository) DeleteByEmailAndAccountID(email string, accountID int) error { + _, err := r.db.Exec(` + DELETE FROM invitations WHERE invited_email = $1 AND account_id = $2 + `, email, accountID) + return err +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..f7719dc --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,95 @@ +package router + +import ( + "database/sql" + "net/http" + + "gpt-manager-go/internal/handler" + "gpt-manager-go/internal/middleware" + "gpt-manager-go/internal/repository" + "gpt-manager-go/internal/service" +) + +// SetupRoutes 设置路由 +func SetupRoutes(db *sql.DB) http.Handler { + mux := http.NewServeMux() + + // 初始化仓储 + adminRepo := repository.NewAdminRepository(db) + chatgptAccountRepo := repository.NewChatGPTAccountRepository(db) + invitationRepo := repository.NewInvitationRepository(db) + cardKeyRepo := repository.NewCardKeyRepository(db) + + // 初始化服务 + chatgptService := service.NewChatGPTService() + + // 初始化处理器 + authHandler := handler.NewAuthHandler(adminRepo) + accountHandler := handler.NewChatGPTAccountHandler(chatgptAccountRepo, chatgptService) + inviteHandler := handler.NewInviteHandler(chatgptAccountRepo, invitationRepo, cardKeyRepo, chatgptService) + cardKeyHandler := handler.NewCardKeyHandler(cardKeyRepo) + + // 公开路由 (无需认证) + mux.HandleFunc("/api/login", authHandler.Login) + mux.HandleFunc("/api/invite/card", inviteHandler.InviteByCardKey) // 卡密邀请 + + // 需要认证的路由 + protectedMux := http.NewServeMux() + protectedMux.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) { + claims := middleware.GetUserFromContext(r.Context()) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"success":true,"user":{"id":` + itoa(claims.UserID) + `,"username":"` + claims.Username + `"}}`)) + }) + + // ChatGPT 账号管理接口 + protectedMux.HandleFunc("/api/accounts", accountHandler.List) + protectedMux.HandleFunc("/api/accounts/create", accountHandler.Create) + protectedMux.HandleFunc("/api/accounts/refresh", accountHandler.Refresh) + protectedMux.HandleFunc("/api/accounts/delete", accountHandler.Delete) + + // 邀请接口 (管理员) - POST: 邀请, DELETE: 移除 + protectedMux.HandleFunc("/api/invite", inviteHandler.InviteByAdmin) + + // 卡密管理接口 + protectedMux.HandleFunc("/api/cardkeys", cardKeyHandler.Handle) // GET: 列表, POST: 创建 + protectedMux.HandleFunc("/api/cardkeys/batch", cardKeyHandler.BatchCreate) // POST: 批量创建 + + // 挂载受保护的路由 + mux.Handle("/api/profile", middleware.AuthMiddleware(protectedMux)) + mux.Handle("/api/accounts", middleware.AuthMiddleware(protectedMux)) + mux.Handle("/api/accounts/", middleware.AuthMiddleware(protectedMux)) + mux.Handle("/api/invite", middleware.AuthMiddleware(protectedMux)) + mux.Handle("/api/cardkeys", middleware.AuthMiddleware(protectedMux)) + mux.Handle("/api/cardkeys/", middleware.AuthMiddleware(protectedMux)) + + // CORS 中间件包装 + return corsMiddleware(mux) +} + +// corsMiddleware CORS 中间件 +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +func itoa(i int) string { + if i == 0 { + return "0" + } + var s string + for i > 0 { + s = string(rune('0'+i%10)) + s + i /= 10 + } + return s +} diff --git a/internal/service/chatgpt_service.go b/internal/service/chatgpt_service.go new file mode 100644 index 0000000..548ecda --- /dev/null +++ b/internal/service/chatgpt_service.go @@ -0,0 +1,229 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// ChatGPTService ChatGPT API 服务 +type ChatGPTService struct { + client *http.Client + baseURL string +} + +// NewChatGPTService 创建 ChatGPT 服务 +func NewChatGPTService() *ChatGPTService { + return &ChatGPTService{ + client: &http.Client{ + Timeout: 30 * time.Second, + }, + baseURL: "https://chatgpt.com/backend-api", + } +} + +// SubscriptionInfo 订阅信息 +type SubscriptionInfo struct { + SeatsInUse int `json:"seats_in_use"` + SeatsEntitled int `json:"seats_entitled"` + ActiveStart time.Time `json:"active_start"` + ActiveUntil time.Time `json:"active_until"` + IsValid bool `json:"is_valid"` +} + +// subscriptionResponse ChatGPT API 响应结构 +type subscriptionResponse struct { + SeatsInUse int `json:"seats_in_use"` + SeatsEntitled int `json:"seats_entitled"` + ActiveStart string `json:"active_start"` + ActiveUntil string `json:"active_until"` +} + +// GetSubscription 获取订阅信息 +func (s *ChatGPTService) GetSubscription(teamAccountID, authToken string) (*SubscriptionInfo, error) { + url := fmt.Sprintf("%s/subscriptions?account_id=%s", s.baseURL, teamAccountID) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // 设置请求头 + req.Header.Set("Authorization", authToken) + req.Header.Set("chatgpt-account-id", teamAccountID) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + // 检查响应状态码 + if resp.StatusCode != http.StatusOK { + return &SubscriptionInfo{IsValid: false}, nil + } + + var subResp subscriptionResponse + if err := json.Unmarshal(body, &subResp); err != nil { + return &SubscriptionInfo{IsValid: false}, nil + } + + // 解析时间 + info := &SubscriptionInfo{ + SeatsInUse: subResp.SeatsInUse, + SeatsEntitled: subResp.SeatsEntitled, + IsValid: true, + } + + if subResp.ActiveStart != "" { + if t, err := time.Parse(time.RFC3339, subResp.ActiveStart); err == nil { + info.ActiveStart = t + } + } + if subResp.ActiveUntil != "" { + if t, err := time.Parse(time.RFC3339, subResp.ActiveUntil); err == nil { + info.ActiveUntil = t + } + } + + return info, nil +} + +// InviteRequest 邀请请求结构 +type InviteRequest struct { + EmailAddresses []string `json:"email_addresses"` + Role string `json:"role"` + ResendEmails bool `json:"resend_emails"` +} + +// InviteResponse 邀请响应 +type InviteResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + RawBody string `json:"-"` +} + +// SendInvite 发送邀请到 ChatGPT Team +func (s *ChatGPTService) SendInvite(teamAccountID, authToken, email string) (*InviteResponse, error) { + url := fmt.Sprintf("%s/accounts/%s/invites", s.baseURL, teamAccountID) + + // 构建请求体 + reqBody := InviteRequest{ + EmailAddresses: []string{email}, + Role: "standard-user", + ResendEmails: true, + } + + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // 设置请求头 + req.Header.Set("Authorization", authToken) + req.Header.Set("chatgpt-account-id", teamAccountID) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + // 检查响应状态码 + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return &InviteResponse{ + Success: true, + Message: "Invitation sent successfully", + RawBody: string(body), + }, nil + } + + // 解析错误响应 + return &InviteResponse{ + Success: false, + Message: fmt.Sprintf("API returned status %d: %s", resp.StatusCode, string(body)), + RawBody: string(body), + }, nil +} + +// RemoveMemberResponse 移除成员响应 +type RemoveMemberResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + RawBody string `json:"-"` +} + +// RemoveMember 从 ChatGPT Team 移除成员 +func (s *ChatGPTService) RemoveMember(teamAccountID, authToken, email string) (*RemoveMemberResponse, error) { + url := fmt.Sprintf("%s/accounts/%s/invites", s.baseURL, teamAccountID) + + // 构建请求体 - 只需要邮箱 + reqBody := map[string]string{ + "email_address": email, + } + + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("DELETE", url, bytes.NewBuffer(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // 设置请求头 + req.Header.Set("Authorization", authToken) + req.Header.Set("chatgpt-account-id", teamAccountID) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + // 检查响应状态码 + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return &RemoveMemberResponse{ + Success: true, + Message: "Member removed successfully", + RawBody: string(body), + }, nil + } + + // 解析错误响应 + return &RemoveMemberResponse{ + Success: false, + Message: fmt.Sprintf("API returned status %d: %s", resp.StatusCode, string(body)), + RawBody: string(body), + }, nil +}