feat: 初始化 ChatGPT Team 管理后端项目
- 添加用户认证模块 (JWT + 密码管理) - 添加 ChatGPT 账户管理功能 - 添加卡密管理功能 (创建、批量生成、查询) - 添加邀请功能 - 配置数据库迁移和路由系统
This commit is contained in:
18
.env.example
Normal file
18
.env.example
Normal file
@@ -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
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -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
|
||||
43
cmd/main.go
Normal file
43
cmd/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
60
cmd/seed/main.go
Normal file
60
cmd/seed/main.go
Normal file
@@ -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")
|
||||
}
|
||||
156
database_schema.md
Normal file
156
database_schema.md
Normal file
@@ -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 │
|
||||
└──────────────────┘
|
||||
```
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -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
|
||||
)
|
||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -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=
|
||||
66
internal/auth/jwt.go
Normal file
66
internal/auth/jwt.go
Normal file
@@ -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"
|
||||
}
|
||||
17
internal/auth/password.go
Normal file
17
internal/auth/password.go
Normal file
@@ -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
|
||||
}
|
||||
49
internal/config/env.go
Normal file
49
internal/config/env.go
Normal file
@@ -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()
|
||||
}
|
||||
99
internal/db/db.go
Normal file
99
internal/db/db.go
Normal file
@@ -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
|
||||
}
|
||||
204
internal/db/migrations.go
Normal file
204
internal/db/migrations.go
Normal file
@@ -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;
|
||||
`
|
||||
138
internal/handler/auth_handler.go
Normal file
138
internal/handler/auth_handler.go
Normal file
@@ -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)
|
||||
}
|
||||
213
internal/handler/card_key_handler.go
Normal file
213
internal/handler/card_key_handler.go
Normal file
@@ -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]
|
||||
}
|
||||
287
internal/handler/chatgpt_account_handler.go
Normal file
287
internal/handler/chatgpt_account_handler.go
Normal file
@@ -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",
|
||||
})
|
||||
}
|
||||
393
internal/handler/invite_handler.go
Normal file
393
internal/handler/invite_handler.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
65
internal/middleware/auth_middleware.go
Normal file
65
internal/middleware/auth_middleware.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
23
internal/models/admin.go
Normal file
23
internal/models/admin.go
Normal file
@@ -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"
|
||||
}
|
||||
25
internal/models/api_key.go
Normal file
25
internal/models/api_key.go
Normal file
@@ -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"
|
||||
}
|
||||
42
internal/models/card_key.go
Normal file
42
internal/models/card_key.go
Normal file
@@ -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
|
||||
}
|
||||
109
internal/models/chatgpt_account.go
Normal file
109
internal/models/chatgpt_account.go
Normal file
@@ -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)
|
||||
}
|
||||
43
internal/models/invitation.go
Normal file
43
internal/models/invitation.go
Normal file
@@ -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)
|
||||
}
|
||||
42
internal/models/system_setting.go
Normal file
42
internal/models/system_setting.go
Normal file
@@ -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"
|
||||
)
|
||||
120
internal/repository/admin_repo.go
Normal file
120
internal/repository/admin_repo.go
Normal file
@@ -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
|
||||
}
|
||||
89
internal/repository/card_key_repo.go
Normal file
89
internal/repository/card_key_repo.go
Normal file
@@ -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
|
||||
}
|
||||
207
internal/repository/chatgpt_account_repo.go
Normal file
207
internal/repository/chatgpt_account_repo.go
Normal file
@@ -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
|
||||
}
|
||||
83
internal/repository/invitation_repo.go
Normal file
83
internal/repository/invitation_repo.go
Normal file
@@ -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
|
||||
}
|
||||
95
internal/router/router.go
Normal file
95
internal/router/router.go
Normal file
@@ -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
|
||||
}
|
||||
229
internal/service/chatgpt_service.go
Normal file
229
internal/service/chatgpt_service.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user