feat: 初始化 ChatGPT Team 管理后端项目

- 添加用户认证模块 (JWT + 密码管理)
- 添加 ChatGPT 账户管理功能
- 添加卡密管理功能 (创建、批量生成、查询)
- 添加邀请功能
- 配置数据库迁移和路由系统
This commit is contained in:
sar
2026-01-13 14:42:56 +08:00
commit 42c423bd32
29 changed files with 2969 additions and 0 deletions

99
internal/db/db.go Normal file
View 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
View 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;
`