Files
Sarteambot Admin 0fde6d4a0b feat: 初始化 ChatGPT Team 管理机器人
核心功能:
- 实现基于 Telegram Inline Button 交互的后台面板与用户端
- 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号
- 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请
- 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码
- 随机邀请功能:成功拉人后自动核销兑换码
- 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误)

系统与配置:
- 使用 PostgreSQL 数据库管理账号、邀请和兑换记录
- 支持在端内动态添加、移除管理员
- 完善 Docker 部署配置与 .gitignore 规则
2026-03-04 20:08:34 +08:00

460 lines
14 KiB
Go

package database
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq"
"go-helper/internal/model"
)
// DB wraps the sql.DB connection and provides typed query methods.
type DB struct {
*sql.DB
}
// New opens a PostgreSQL connection and auto-creates tables.
func New(databaseURL string) *DB {
conn, err := sql.Open("postgres", databaseURL)
if err != nil {
log.Fatalf("[DB] 无法连接数据库: %v", err)
}
if err := conn.Ping(); err != nil {
log.Fatalf("[DB] 数据库连接测试失败: %v", err)
}
conn.SetMaxOpenConns(10)
conn.SetMaxIdleConns(5)
conn.SetConnMaxLifetime(5 * time.Minute)
d := &DB{conn}
d.migrate()
log.Println("[DB] 数据库初始化完成")
return d
}
func (d *DB) migrate() {
queries := []string{
`CREATE TABLE IF NOT EXISTS gpt_accounts (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL,
token TEXT NOT NULL,
refresh_token TEXT DEFAULT '',
user_count INT DEFAULT 0,
invite_count INT DEFAULT 0,
chatgpt_account_id TEXT DEFAULT '',
oai_device_id TEXT DEFAULT '',
expire_at TEXT DEFAULT '',
is_open BOOLEAN DEFAULT TRUE,
is_banned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS redemption_codes (
id BIGSERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
is_redeemed BOOLEAN DEFAULT FALSE,
redeemed_at TEXT,
redeemed_by TEXT,
account_email TEXT DEFAULT '',
channel TEXT DEFAULT 'common',
created_at TIMESTAMP DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS telegram_admins (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL,
added_by BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)`,
}
for _, q := range queries {
if _, err := d.Exec(q); err != nil {
log.Fatalf("[DB] 建表失败: %v", err)
}
}
}
// --------------- GptAccount CRUD ---------------
// GetAllAccounts returns all accounts ordered by creation time desc.
func (d *DB) GetAllAccounts() ([]model.GptAccount, error) {
rows, err := d.Query(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanAccounts(rows)
}
// GetOpenAccounts returns non-banned, open accounts that still have capacity.
func (d *DB) GetOpenAccounts(capacity int) ([]model.GptAccount, error) {
rows, err := d.Query(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts
WHERE is_open = TRUE AND is_banned = FALSE
AND (user_count + invite_count) < $1
AND token != '' AND chatgpt_account_id != ''
ORDER BY (user_count + invite_count) ASC, RANDOM()`, capacity)
if err != nil {
return nil, err
}
defer rows.Close()
return scanAccounts(rows)
}
// GetAccountByID fetches a single account by its ID.
func (d *DB) GetAccountByID(id int64) (*model.GptAccount, error) {
row := d.QueryRow(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts WHERE id = $1 LIMIT 1`, id)
return scanAccount(row)
}
// GetAccountByChatGPTAccountID fetches a single account by its OpenAI account ID.
func (d *DB) GetAccountByChatGPTAccountID(accountID string) (*model.GptAccount, error) {
row := d.QueryRow(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts WHERE chatgpt_account_id = $1 LIMIT 1`, accountID)
return scanAccount(row)
}
// GetAccountByEmail fetches a single account by email (case-insensitive).
func (d *DB) GetAccountByEmail(email string) (*model.GptAccount, error) {
row := d.QueryRow(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts WHERE lower(email) = lower($1) LIMIT 1`, email)
return scanAccount(row)
}
// GetAccountsWithRT returns accounts that have a refresh token and are not banned.
func (d *DB) GetAccountsWithRT() ([]model.GptAccount, error) {
rows, err := d.Query(`
SELECT id, email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned,
created_at, updated_at
FROM gpt_accounts
WHERE refresh_token != '' AND is_banned = FALSE
ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanAccounts(rows)
}
// CreateAccount inserts a new account and returns its ID.
func (d *DB) CreateAccount(a *model.GptAccount) (int64, error) {
var id int64
err := d.QueryRow(`
INSERT INTO gpt_accounts (email, token, refresh_token, user_count, invite_count,
chatgpt_account_id, oai_device_id, expire_at, is_open, is_banned)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id`,
a.Email, a.Token, a.RefreshToken, a.UserCount, a.InviteCount,
a.ChatgptAccountID, a.OaiDeviceID, a.ExpireAt, a.IsOpen, a.IsBanned,
).Scan(&id)
return id, err
}
// UpdateAccountTokens updates the access token and refresh token.
func (d *DB) UpdateAccountTokens(id int64, accessToken, refreshToken string) error {
_, err := d.Exec(`
UPDATE gpt_accounts
SET token = $1, refresh_token = $2, updated_at = NOW()
WHERE id = $3`, accessToken, refreshToken, id)
return err
}
// UpdateAccountEmail updates the email of an account and its associated codes.
func (d *DB) UpdateAccountEmail(id int64, oldEmail, newEmail string) error {
tx, err := d.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec(`
UPDATE gpt_accounts
SET email = $1, updated_at = NOW()
WHERE id = $2`, newEmail, id)
if err != nil {
return err
}
_, err = tx.Exec(`
UPDATE redemption_codes
SET account_email = $1
WHERE account_email = $2`, newEmail, oldEmail)
if err != nil {
return err
}
return tx.Commit()
}
// UpdateAccountCounts updates user_count and invite_count.
func (d *DB) UpdateAccountCounts(id int64, userCount, inviteCount int) error {
_, err := d.Exec(`
UPDATE gpt_accounts
SET user_count = $1, invite_count = $2, updated_at = NOW()
WHERE id = $3`, userCount, inviteCount, id)
return err
}
// UpdateAccountInfo updates chatgpt_account_id, expire_at, etc. from API data.
func (d *DB) UpdateAccountInfo(id int64, chatgptAccountID, expireAt string) error {
_, err := d.Exec(`
UPDATE gpt_accounts
SET chatgpt_account_id = $1, expire_at = $2, updated_at = NOW()
WHERE id = $3`, chatgptAccountID, expireAt, id)
return err
}
// BanAccount marks an account as banned and not open.
func (d *DB) BanAccount(id int64) error {
_, err := d.Exec(`
UPDATE gpt_accounts
SET is_banned = TRUE, is_open = FALSE, updated_at = NOW()
WHERE id = $1`, id)
return err
}
// --------------- RedemptionCode CRUD ---------------
// GetCodeByCode fetches a redemption code by its code string.
func (d *DB) GetCodeByCode(code string) (*model.RedemptionCode, error) {
row := d.QueryRow(`
SELECT id, code, is_redeemed, redeemed_at, redeemed_by, account_email, channel, created_at
FROM redemption_codes WHERE code = $1`, code)
var rc model.RedemptionCode
err := row.Scan(&rc.ID, &rc.Code, &rc.IsRedeemed, &rc.RedeemedAt, &rc.RedeemedBy,
&rc.AccountEmail, &rc.Channel, &rc.CreatedAt)
if err != nil {
return nil, err
}
return &rc, nil
}
// CreateCode inserts a batch of redemption codes for a given account email.
func (d *DB) CreateCodes(accountEmail string, codes []string) error {
tx, err := d.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO redemption_codes (code, account_email) VALUES ($1, $2)
ON CONFLICT (code) DO NOTHING`)
if err != nil {
return err
}
defer stmt.Close()
for _, c := range codes {
if _, err := stmt.Exec(c, accountEmail); err != nil {
return err
}
}
return tx.Commit()
}
// RedeemCode marks a code as redeemed.
func (d *DB) RedeemCode(codeID int64, redeemedBy string) error {
_, err := d.Exec(`
UPDATE redemption_codes
SET is_redeemed = TRUE, redeemed_at = NOW()::TEXT, redeemed_by = $1
WHERE id = $2`, redeemedBy, codeID)
return err
}
// CountAvailableCodes returns the number of unredeemed codes.
func (d *DB) CountAvailableCodes() (int, error) {
var count int
err := d.QueryRow(`SELECT COUNT(*) FROM redemption_codes WHERE is_redeemed = FALSE`).Scan(&count)
return count, err
}
// CountAvailableCodesByAccount returns the number of unredeemed codes for a specific account.
func (d *DB) CountAvailableCodesByAccount(accountEmail string) (int, error) {
var count int
err := d.QueryRow(`
SELECT COUNT(*) FROM redemption_codes
WHERE is_redeemed = FALSE AND account_email = $1`, accountEmail).Scan(&count)
return count, err
}
// GetCodesByAccount returns all codes for an account email.
func (d *DB) GetCodesByAccount(accountEmail string) ([]model.RedemptionCode, error) {
rows, err := d.Query(`
SELECT id, code, is_redeemed, redeemed_at, redeemed_by, account_email, channel, created_at
FROM redemption_codes WHERE account_email = $1 ORDER BY id`, accountEmail)
if err != nil {
return nil, err
}
defer rows.Close()
var list []model.RedemptionCode
for rows.Next() {
var rc model.RedemptionCode
if err := rows.Scan(&rc.ID, &rc.Code, &rc.IsRedeemed, &rc.RedeemedAt, &rc.RedeemedBy,
&rc.AccountEmail, &rc.Channel, &rc.CreatedAt); err != nil {
return nil, err
}
list = append(list, rc)
}
return list, rows.Err()
}
// GetAllCodes returns all redemption codes.
func (d *DB) GetAllCodes() ([]model.RedemptionCode, error) {
rows, err := d.Query(`
SELECT id, code, is_redeemed, redeemed_at, redeemed_by, account_email, channel, created_at
FROM redemption_codes ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var list []model.RedemptionCode
for rows.Next() {
var rc model.RedemptionCode
if err := rows.Scan(&rc.ID, &rc.Code, &rc.IsRedeemed, &rc.RedeemedAt, &rc.RedeemedBy,
&rc.AccountEmail, &rc.Channel, &rc.CreatedAt); err != nil {
return nil, err
}
list = append(list, rc)
}
return list, rows.Err()
}
// DeleteCode deletes a specific redemption code.
func (d *DB) DeleteCode(code string) error {
_, err := d.Exec(`DELETE FROM redemption_codes WHERE code = $1`, code)
return err
}
// DeleteUnusedCodes deletes all redemption codes that haven't been redeemed.
// Returns the number of codes deleted.
func (d *DB) DeleteUnusedCodes() (int64, error) {
res, err := d.Exec(`DELETE FROM redemption_codes WHERE is_redeemed = false`)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// DeleteAccount removes an account and its associated codes.
func (d *DB) DeleteAccount(id int64) error {
// Get account email first.
acct, err := d.GetAccountByID(id)
if err != nil {
return err
}
// Delete associated redemption codes (using the current email).
_, _ = d.Exec(`DELETE FROM redemption_codes WHERE account_email = $1`, acct.Email)
// Delete the account.
_, err = d.Exec(`DELETE FROM gpt_accounts WHERE id = $1`, id)
// Also clean up any orphaned codes that don't match any existing account email.
// This fixes issues where codes were generated under a team name before the email was updated.
_, _ = d.Exec(`DELETE FROM redemption_codes WHERE account_email NOT IN (SELECT email FROM gpt_accounts)`)
return err
}
// --------------- TelegramAdmin CRUD ---------------
// AddAdmin inserts a new admin into the database.
func (d *DB) AddAdmin(userID int64, addedBy int64) error {
_, err := d.Exec(`
INSERT INTO telegram_admins (user_id, added_by)
VALUES ($1, $2)
ON CONFLICT (user_id) DO NOTHING`, userID, addedBy)
return err
}
// RemoveAdmin deletes an admin from the database by user ID.
func (d *DB) RemoveAdmin(userID int64) error {
_, err := d.Exec(`DELETE FROM telegram_admins WHERE user_id = $1`, userID)
return err
}
// GetAllAdmins returns a list of all admins stored in the database.
func (d *DB) GetAllAdmins() ([]model.TelegramAdmin, error) {
rows, err := d.Query(`SELECT id, user_id, added_by, created_at FROM telegram_admins ORDER BY created_at ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
var list []model.TelegramAdmin
for rows.Next() {
var a model.TelegramAdmin
if err := rows.Scan(&a.ID, &a.UserID, &a.AddedBy, &a.CreatedAt); err != nil {
return nil, err
}
list = append(list, a)
}
return list, nil
}
// IsAdmin checks if a specific user ID exists in the admin table.
func (d *DB) IsAdmin(userID int64) (bool, error) {
var count int
err := d.QueryRow(`SELECT count(*) FROM telegram_admins WHERE user_id = $1`, userID).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// --------------- helpers ---------------
func scanAccounts(rows *sql.Rows) ([]model.GptAccount, error) {
var list []model.GptAccount
for rows.Next() {
a, err := scanAccountFromRows(rows)
if err != nil {
return nil, err
}
list = append(list, *a)
}
return list, rows.Err()
}
func scanAccountFromRows(rows *sql.Rows) (*model.GptAccount, error) {
var a model.GptAccount
err := rows.Scan(&a.ID, &a.Email, &a.Token, &a.RefreshToken,
&a.UserCount, &a.InviteCount, &a.ChatgptAccountID, &a.OaiDeviceID,
&a.ExpireAt, &a.IsOpen, &a.IsBanned, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
return &a, nil
}
func scanAccount(row *sql.Row) (*model.GptAccount, error) {
var a model.GptAccount
err := row.Scan(&a.ID, &a.Email, &a.Token, &a.RefreshToken,
&a.UserCount, &a.InviteCount, &a.ChatgptAccountID, &a.OaiDeviceID,
&a.ExpireAt, &a.IsOpen, &a.IsBanned, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("账号不存在: %w", err)
}
return &a, nil
}