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

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

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

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