feat: 实现前端卡密管理界面

- 卡密列表展示与分页功能

- 单个/批量创建卡密

- 卡密删除与批量删除

- 卡密导出功能 (file-saver)

- 启用/禁用状态切换

- 状态判断 (有效/已使用/已失效)

- Toast 通知系统 (vue-sonner)

- 登录页面错误提示优化

- 后端登录错误消息中文化
This commit is contained in:
sar
2026-01-13 21:34:56 +08:00
parent 42c423bd32
commit 8d60704eda
143 changed files with 6646 additions and 91 deletions

View 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"
}

View 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
}

View 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
backend/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
}

View File

@@ -0,0 +1,177 @@
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,
}
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);
`

View 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: "用户名或密码错误",
})
return
}
// 检查账号是否激活
if !admin.IsActive {
respondJSON(w, http.StatusForbidden, LoginResponse{
Success: false,
Message: "账号已被禁用",
})
return
}
// 验证密码
if !auth.CheckPassword(req.Password, admin.PasswordHash) {
respondJSON(w, http.StatusUnauthorized, LoginResponse{
Success: false,
Message: "用户名或密码错误",
})
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)
}

View File

@@ -0,0 +1,367 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"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"`
Total int `json:"total,omitempty"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,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) {
// 获取分页参数
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
page, _ := strconv.Atoi(pageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
// 默认值
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
// 获取总数
total, err := h.repo.Count()
if err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to count card keys",
})
return
}
// 获取分页数据
cardKeys, err := h.repo.FindAllPaginated(page, pageSize)
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,
Total: total,
Page: page,
PageSize: pageSize,
})
}
// 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]
}
// Delete 删除单个卡密 (DELETE /api/cardkeys/delete?id=xxx)
func (h *CardKeyHandler) Delete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
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, CardKeyResponse{
Success: false,
Message: "Invalid card key ID",
})
return
}
if err := h.repo.Delete(id); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to delete card key",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card key deleted successfully",
})
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteRequest struct {
IDs []int `json:"ids"`
}
// BatchDelete 批量删除卡密 (DELETE /api/cardkeys/batch)
func (h *CardKeyHandler) BatchDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req BatchDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if len(req.IDs) == 0 {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "No IDs provided",
})
return
}
if err := h.repo.BatchDelete(req.IDs); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to delete card keys",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card keys deleted successfully",
})
}
// ToggleActiveRequest 切换激活状态请求
type ToggleActiveRequest struct {
ID int `json:"id"`
IsActive bool `json:"is_active"`
}
// ToggleActive 切换卡密激活状态 (POST /api/cardkeys/toggle)
func (h *CardKeyHandler) ToggleActive(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 ToggleActiveRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if err := h.repo.ToggleActive(req.ID, req.IsActive); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to toggle card key status",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card key status updated successfully",
})
}

View File

@@ -0,0 +1,322 @@
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"`
Total int `json:"total,omitempty"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,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
}
// 获取分页参数
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
page, _ := strconv.Atoi(pageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
// 默认值
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
// 获取总数
total, err := h.repo.Count()
if err != nil {
respondJSON(w, http.StatusInternalServerError, ListResponse{
Success: false,
Message: "Failed to count accounts",
})
return
}
// 获取分页数据
accounts, err := h.repo.FindAllPaginated(page, pageSize)
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,
Total: total,
Page: page,
PageSize: pageSize,
})
}
// 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",
})
}

View File

@@ -0,0 +1,445 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"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"`
}
// InviteListResponse 邀请列表响应
type InviteListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data []*models.Invitation `json:"data,omitempty"`
}
// ListByAccount 获取账号的邀请列表 (GET /api/invite?account_id=xxx)
func (h *InviteHandler) ListByAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
respondJSON(w, http.StatusMethodNotAllowed, InviteListResponse{
Success: false,
Message: "Method not allowed",
})
return
}
accountIDStr := r.URL.Query().Get("account_id")
if accountIDStr == "" {
respondJSON(w, http.StatusBadRequest, InviteListResponse{
Success: false,
Message: "account_id is required",
})
return
}
var accountID int
if _, err := fmt.Sscanf(accountIDStr, "%d", &accountID); err != nil {
respondJSON(w, http.StatusBadRequest, InviteListResponse{
Success: false,
Message: "Invalid account_id",
})
return
}
invitations, err := h.invitationRepo.FindByAccountID(accountID)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteListResponse{
Success: false,
Message: "Failed to fetch invitations",
})
return
}
respondJSON(w, http.StatusOK, InviteListResponse{
Success: true,
Message: "OK",
Data: invitations,
})
}
// 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,
})
}

View 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)
})
}

View 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"
}

View 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"
}

View 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
}

View 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)
}

View 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)
}

View 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
}

View File

@@ -0,0 +1,149 @@
package repository
import (
"database/sql"
"gpt-manager-go/internal/models"
"github.com/lib/pq"
)
// 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
}
// FindAllPaginated 分页获取卡密
func (r *CardKeyRepository) FindAllPaginated(page, pageSize int) ([]*models.CardKey, error) {
offset := (page - 1) * pageSize
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
LIMIT $1 OFFSET $2
`, pageSize, offset)
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
}
// Count 获取卡密总数
func (r *CardKeyRepository) Count() (int, error) {
var count int
err := r.db.QueryRow(`SELECT COUNT(*) FROM card_keys`).Scan(&count)
return count, err
}
// Delete 删除单个卡密
func (r *CardKeyRepository) Delete(id int) error {
_, err := r.db.Exec(`DELETE FROM card_keys WHERE id = $1`, id)
return err
}
// BatchDelete 批量删除卡密
func (r *CardKeyRepository) BatchDelete(ids []int) error {
if len(ids) == 0 {
return nil
}
// 构建 IN 子句
query := `DELETE FROM card_keys WHERE id = ANY($1)`
_, err := r.db.Exec(query, pq.Array(ids))
return err
}
// ToggleActive 切换卡密激活状态
func (r *CardKeyRepository) ToggleActive(id int, isActive bool) error {
_, err := r.db.Exec(`UPDATE card_keys SET is_active = $1 WHERE id = $2`, isActive, id)
return err
}

View File

@@ -0,0 +1,245 @@
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
}
// FindAllPaginated 分页获取账号
func (r *ChatGPTAccountRepository) FindAllPaginated(page, pageSize int) ([]*models.ChatGPTAccount, error) {
offset := (page - 1) * pageSize
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
LIMIT $1 OFFSET $2
`, pageSize, offset)
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
}
// Count 获取账号总数
func (r *ChatGPTAccountRepository) Count() (int, error) {
var count int
err := r.db.QueryRow(`SELECT COUNT(*) FROM chatgpt_accounts`).Scan(&count)
return count, err
}

View File

@@ -0,0 +1,109 @@
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
}
// FindByAccountID 根据账号ID查找邀请记录
func (r *InvitationRepository) FindByAccountID(accountID int) ([]*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 account_id = $1 ORDER BY created_at DESC
`, accountID)
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
}

View File

@@ -0,0 +1,119 @@
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)
// 邀请接口 (管理员) - GET: 列表, POST: 邀请, DELETE: 移除
protectedMux.HandleFunc("/api/invite", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
inviteHandler.ListByAccount(w, r)
case http.MethodPost, http.MethodDelete:
inviteHandler.InviteByAdmin(w, r)
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(`{"success":false,"message":"Method not allowed"}`))
}
})
// 卡密管理接口
protectedMux.HandleFunc("/api/cardkeys", cardKeyHandler.Handle) // GET: 列表, POST: 创建
protectedMux.HandleFunc("/api/cardkeys/batch", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
cardKeyHandler.BatchCreate(w, r)
case http.MethodDelete:
cardKeyHandler.BatchDelete(w, r)
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(`{"success":false,"message":"Method not allowed"}`))
}
})
protectedMux.HandleFunc("/api/cardkeys/delete", cardKeyHandler.Delete) // DELETE: 单个删除
protectedMux.HandleFunc("/api/cardkeys/toggle", cardKeyHandler.ToggleActive) // 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
}

View 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
}