chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
206
internal/store/postgres/stars.go
Normal file
206
internal/store/postgres/stars.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// StarsStore 用 PostgreSQL 实现 store.StarsStore(Stars 本地账本)。
|
||||
// 借记/贷记/授予各自在单事务内完成:余额与流水永不漂移。
|
||||
type StarsStore struct {
|
||||
db sqlcgen.DBTX
|
||||
}
|
||||
|
||||
// NewStarsStore 基于 pgx 连接池(或事务)创建 StarsStore。
|
||||
func NewStarsStore(db sqlcgen.DBTX) *StarsStore {
|
||||
return &StarsStore{db: db}
|
||||
}
|
||||
|
||||
func (s *StarsStore) GetBalance(ctx context.Context, userID int64) (domain.StarsBalance, error) {
|
||||
if userID == 0 {
|
||||
return domain.StarsBalance{}, nil
|
||||
}
|
||||
bal := domain.StarsBalance{UserID: userID}
|
||||
err := s.db.QueryRow(ctx, `SELECT balance, granted FROM stars_balances WHERE user_id = $1`, userID).
|
||||
Scan(&bal.Balance, &bal.Granted)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.StarsBalance{UserID: userID}, nil
|
||||
}
|
||||
return domain.StarsBalance{}, fmt.Errorf("get stars balance: %w", err)
|
||||
}
|
||||
return bal, nil
|
||||
}
|
||||
|
||||
func (s *StarsStore) EnsureGrant(ctx context.Context, userID, amount int64, date int) (domain.StarsBalance, bool, error) {
|
||||
if userID == 0 {
|
||||
return domain.StarsBalance{}, false, nil
|
||||
}
|
||||
if amount <= 0 {
|
||||
// 授予额为 0 时只确保有一行且 granted=true(幂等关闭授予)。
|
||||
bal, err := s.GetBalance(ctx, userID)
|
||||
return bal, false, err
|
||||
}
|
||||
out := domain.StarsBalance{UserID: userID}
|
||||
applied := false
|
||||
err := withTx(ctx, s.db, "ensure stars grant", func(tx pgx.Tx) error {
|
||||
var balance int64
|
||||
var granted bool
|
||||
err := tx.QueryRow(ctx, `SELECT balance, granted FROM stars_balances WHERE user_id = $1 FOR UPDATE`, userID).
|
||||
Scan(&balance, &granted)
|
||||
switch {
|
||||
case errors.Is(err, pgx.ErrNoRows):
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO stars_balances (user_id, balance, granted, updated_at) VALUES ($1, $2, true, now())`,
|
||||
userID, amount); err != nil {
|
||||
return fmt.Errorf("insert stars balance grant: %w", err)
|
||||
}
|
||||
out.Balance, out.Granted, applied = amount, true, true
|
||||
case err != nil:
|
||||
return fmt.Errorf("select stars balance for grant: %w", err)
|
||||
case granted:
|
||||
out.Balance, out.Granted = balance, true
|
||||
return nil
|
||||
default:
|
||||
if err := tx.QueryRow(ctx, `UPDATE stars_balances SET balance = balance + $2, granted = true, updated_at = now() WHERE user_id = $1 RETURNING balance`,
|
||||
userID, amount).Scan(&out.Balance); err != nil {
|
||||
return fmt.Errorf("update stars balance grant: %w", err)
|
||||
}
|
||||
out.Granted, applied = true, true
|
||||
}
|
||||
return insertStarsTxn(ctx, tx, userID, amount, domain.StarsReasonGrant, domain.Peer{}, date, "", "")
|
||||
})
|
||||
if err != nil {
|
||||
return domain.StarsBalance{}, false, err
|
||||
}
|
||||
return out, applied, nil
|
||||
}
|
||||
|
||||
func (s *StarsStore) Credit(ctx context.Context, userID, amount int64, reason domain.StarsTransactionReason, peer domain.Peer, date int, title, desc string) (domain.StarsBalance, error) {
|
||||
if userID == 0 || amount <= 0 {
|
||||
return domain.StarsBalance{}, domain.ErrStarsInvalidAmount
|
||||
}
|
||||
out := domain.StarsBalance{UserID: userID}
|
||||
err := withTx(ctx, s.db, "credit stars", func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO stars_balances (user_id, balance, updated_at) VALUES ($1, $2, now())
|
||||
ON CONFLICT (user_id) DO UPDATE SET balance = stars_balances.balance + EXCLUDED.balance, updated_at = now()
|
||||
RETURNING balance, granted`, userID, amount).Scan(&out.Balance, &out.Granted); err != nil {
|
||||
return fmt.Errorf("credit stars balance: %w", err)
|
||||
}
|
||||
return insertStarsTxn(ctx, tx, userID, amount, reason, peer, date, title, desc)
|
||||
})
|
||||
if err != nil {
|
||||
return domain.StarsBalance{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *StarsStore) Debit(ctx context.Context, userID, amount int64, reason domain.StarsTransactionReason, peer domain.Peer, date int, title, desc string) (domain.StarsBalance, error) {
|
||||
if userID == 0 || amount <= 0 {
|
||||
return domain.StarsBalance{}, domain.ErrStarsInvalidAmount
|
||||
}
|
||||
out := domain.StarsBalance{UserID: userID, Granted: true}
|
||||
err := withTx(ctx, s.db, "debit stars", func(tx pgx.Tx) error {
|
||||
var balance int64
|
||||
var granted bool
|
||||
err := tx.QueryRow(ctx, `SELECT balance, granted FROM stars_balances WHERE user_id = $1 FOR UPDATE`, userID).
|
||||
Scan(&balance, &granted)
|
||||
if errors.Is(err, pgx.ErrNoRows) || (err == nil && balance < amount) {
|
||||
return domain.ErrStarsInsufficient
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("select stars balance for debit: %w", err)
|
||||
}
|
||||
if err := tx.QueryRow(ctx, `UPDATE stars_balances SET balance = balance - $2, updated_at = now() WHERE user_id = $1 RETURNING balance`,
|
||||
userID, amount).Scan(&out.Balance); err != nil {
|
||||
return fmt.Errorf("update stars balance debit: %w", err)
|
||||
}
|
||||
out.Granted = granted
|
||||
return insertStarsTxn(ctx, tx, userID, -amount, reason, peer, date, title, desc)
|
||||
})
|
||||
if err != nil {
|
||||
return domain.StarsBalance{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *StarsStore) ListTransactions(ctx context.Context, userID int64, offset string, limit int) (domain.StarsTransactionPage, error) {
|
||||
if userID == 0 {
|
||||
return domain.StarsTransactionPage{}, nil
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxStarsTransactionsLimit {
|
||||
limit = domain.MaxStarsTransactionsLimit
|
||||
}
|
||||
bal, err := s.GetBalance(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.StarsTransactionPage{}, err
|
||||
}
|
||||
page := domain.StarsTransactionPage{Balance: bal.Balance}
|
||||
|
||||
// keyset:多取一条以探测是否还有下一页。
|
||||
args := []any{userID, limit + 1}
|
||||
query := `
|
||||
SELECT id, peer_type, peer_id, amount, reason, title, description, date
|
||||
FROM stars_transactions
|
||||
WHERE user_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT $2`
|
||||
if cursor, ok := domain.DecodeStarsCursor(offset); ok {
|
||||
query = `
|
||||
SELECT id, peer_type, peer_id, amount, reason, title, description, date
|
||||
FROM stars_transactions
|
||||
WHERE user_id = $1 AND id < $3
|
||||
ORDER BY id DESC
|
||||
LIMIT $2`
|
||||
args = append(args, cursor)
|
||||
}
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return domain.StarsTransactionPage{}, fmt.Errorf("list stars transactions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
txns := make([]domain.StarsTransaction, 0, limit)
|
||||
for rows.Next() {
|
||||
var (
|
||||
t domain.StarsTransaction
|
||||
peerType string
|
||||
peerID int64
|
||||
reason string
|
||||
)
|
||||
if err := rows.Scan(&t.ID, &peerType, &peerID, &t.Amount, &reason, &t.Title, &t.Description, &t.Date); err != nil {
|
||||
return domain.StarsTransactionPage{}, fmt.Errorf("scan stars transaction: %w", err)
|
||||
}
|
||||
t.UserID = userID
|
||||
t.Reason = domain.StarsTransactionReason(reason)
|
||||
if peerType != "" {
|
||||
t.Peer = domain.Peer{Type: domain.PeerType(peerType), ID: peerID}
|
||||
}
|
||||
txns = append(txns, t)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return domain.StarsTransactionPage{}, fmt.Errorf("iterate stars transactions: %w", err)
|
||||
}
|
||||
if len(txns) > limit {
|
||||
txns = txns[:limit]
|
||||
page.NextOffset = domain.EncodeStarsCursor(txns[len(txns)-1].ID)
|
||||
}
|
||||
page.Transactions = txns
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// insertStarsTxn 在事务内写一条流水(amount 带符号)。
|
||||
func insertStarsTxn(ctx context.Context, tx pgx.Tx, userID, amount int64, reason domain.StarsTransactionReason, peer domain.Peer, date int, title, desc string) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO stars_transactions (user_id, peer_type, peer_id, amount, reason, title, description, date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
userID, string(peer.Type), peer.ID, amount, string(reason), title, desc, date); err != nil {
|
||||
return fmt.Errorf("insert stars transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue