Files

167 lines
5.2 KiB
Go

package db
import (
"context"
"fmt"
"time"
"github.com/emil/deepres/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type DB struct {
pool *pgxpool.Pool
}
func New(databaseURL string) (*DB, error) {
pool, err := pgxpool.New(context.Background(), databaseURL)
if err != nil {
return nil, fmt.Errorf("connect to db: %w", err)
}
return &DB{pool: pool}, nil
}
func (db *DB) Close() { db.pool.Close() }
// ---- Users ----
func (db *DB) GetOrCreateUser(ctx context.Context, tgID int64, username, name string) (*models.User, error) {
u := &models.User{}
err := db.pool.QueryRow(ctx, `
INSERT INTO users (tg_id, username, name) VALUES ($1, $2, $3)
ON CONFLICT (tg_id) DO UPDATE SET username=$2, name=$3
RETURNING tg_id, username, name, stars_balance, created_at
`, tgID, username, name).Scan(&u.TelegramID, &u.Username, &u.Name, &u.StarsBalance, &u.CreatedAt)
if err != nil {
return nil, fmt.Errorf("get or create user: %w", err)
}
return u, nil
}
func (db *DB) GetUser(ctx context.Context, tgID int64) (*models.User, error) {
u := &models.User{}
err := db.pool.QueryRow(ctx,
"SELECT tg_id, username, name, stars_balance, created_at FROM users WHERE tg_id=$1", tgID,
).Scan(&u.TelegramID, &u.Username, &u.Name, &u.StarsBalance, &u.CreatedAt)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return u, nil
}
// ---- Research ----
func (db *DB) CreateResearch(ctx context.Context, r *models.Research) error {
r.ID = uuid.New()
r.Status = "pending"
r.CreatedAt = time.Now()
_, err := db.pool.Exec(ctx, `
INSERT INTO researches (id, tg_id, query, mode, status, stars_cost, created_at)
VALUES ($1,$2,$3,$4,$5,$6,$7)
`, r.ID, r.TelegramID, r.Query, r.Mode, r.Status, r.StarsCost, r.CreatedAt)
return err
}
func (db *DB) UpdateResearch(ctx context.Context, id uuid.UUID, status, summary string, reportPath string, sources []byte, errMsg *string) error {
now := time.Now()
_, err := db.pool.Exec(ctx, `
UPDATE researches SET status=$2, summary=$3, report_path=$4, sources=$5, error_message=$6, completed_at=$7
WHERE id=$1
`, id, status, summary, reportPath, sources, errMsg, now)
return err
}
func (db *DB) GetResearch(ctx context.Context, id uuid.UUID) (*models.Research, error) {
r := &models.Research{}
err := db.pool.QueryRow(ctx, "SELECT id,tg_id,query,mode,status,stars_cost,summary,report_path,sources,error_message,created_at,completed_at FROM researches WHERE id=$1", id).
Scan(&r.ID, &r.TelegramID, &r.Query, &r.Mode, &r.Status, &r.StarsCost, &r.Summary, &r.ReportPath, &r.SourcesJSON, &r.ErrorMessage, &r.CreatedAt, &r.CompletedAt)
if err != nil {
return nil, err
}
return r, nil
}
// ---- Transactions ----
// AddTransaction добавляет транзакцию и обновляет баланс атомарно
func (db *DB) AddTransaction(ctx context.Context, t *models.Transaction) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
t.ID = uuid.New()
t.CreatedAt = time.Now()
_, err = tx.Exec(ctx, `
INSERT INTO transactions (id, tg_id, amount, type, description, telegram_charge_id, created_at)
VALUES ($1,$2,$3,$4,$5,$6,$7)
`, t.ID, t.TelegramID, t.Amount, t.Type, t.Description, t.TelegramChargeID, t.CreatedAt)
if err != nil {
return fmt.Errorf("insert transaction: %w", err)
}
// Обновляем баланс
_, err = tx.Exec(ctx, "UPDATE users SET stars_balance = stars_balance + $1 WHERE tg_id=$2", t.Amount, t.TelegramID)
if err != nil {
return fmt.Errorf("update balance: %w", err)
}
return tx.Commit(ctx)
}
// DeductResearchCost списывает звёзды за ресерч атомарно
func (db *DB) DeductResearchCost(ctx context.Context, userID int64, researchID string, cost int, description string) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
// Проверяем баланс
var balance int
err = tx.QueryRow(ctx, "SELECT stars_balance FROM users WHERE tg_id=$1 FOR UPDATE", userID).Scan(&balance)
if err != nil {
return fmt.Errorf("get balance: %w", err)
}
if balance < cost {
return fmt.Errorf("insufficient balance: %d < %d", balance, cost)
}
// Создаём транзакцию
_, err = tx.Exec(ctx, `
INSERT INTO transactions (id, tg_id, amount, type, description, created_at)
VALUES (gen_random_uuid(), $1, $2, 'research', $3, now())
`, userID, -cost, description)
if err != nil {
return fmt.Errorf("insert transaction: %w", err)
}
// Списываем
_, err = tx.Exec(ctx, "UPDATE users SET stars_balance = stars_balance - $1 WHERE tg_id=$2", cost, userID)
if err != nil {
return fmt.Errorf("deduct balance: %w", err)
}
return tx.Commit(ctx)
}
func (db *DB) UpdateStarsBalance(ctx context.Context, tgID int64, delta int) error {
_, err := db.pool.Exec(ctx, "UPDATE users SET stars_balance = stars_balance + $1 WHERE tg_id=$2", delta, tgID)
return err
}
func (db *DB) GetTodayFastCount(ctx context.Context, tgID int64) (int, error) {
var count int
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM researches
WHERE tg_id=$1 AND mode='fast' AND created_at > CURRENT_DATE
`, tgID).Scan(&count)
return count, err
}