Files
gpt-plus-gpt/pkg/provider/card/pool.go
2026-03-15 20:48:19 +08:00

147 lines
3.6 KiB
Go

package card
import (
"fmt"
"log"
"sync"
"time"
)
// CardPool manages a pool of cards with TTL expiration and multi-bind support.
type CardPool struct {
mu sync.Mutex
cards []*poolEntry
index int
ttl time.Duration // card validity duration (0 = no expiry)
multiBind bool // allow reusing cards multiple times
maxBinds int // max binds per card (0 = unlimited when multiBind=true)
}
type poolEntry struct {
card *CardInfo
addedAt time.Time
bindCount int
rejected bool // upstream rejected this card
}
// PoolConfig configures the card pool behavior.
type PoolConfig struct {
TTL time.Duration // card validity period (default: 1h)
MultiBind bool // allow one card to be used multiple times
MaxBinds int // max uses per card (0 = unlimited)
}
// NewCardPool creates a card pool with the given config.
func NewCardPool(cfg PoolConfig) *CardPool {
if cfg.TTL == 0 {
cfg.TTL = time.Hour
}
return &CardPool{
ttl: cfg.TTL,
multiBind: cfg.MultiBind,
maxBinds: cfg.MaxBinds,
}
}
// AddCards adds cards to the pool.
func (p *CardPool) AddCards(cards []*CardInfo) {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
for _, c := range cards {
p.cards = append(p.cards, &poolEntry{
card: c,
addedAt: now,
})
}
}
// GetCard returns the next available card from the pool.
func (p *CardPool) GetCard() (*CardInfo, error) {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
checked := 0
for checked < len(p.cards) {
entry := p.cards[p.index]
p.index = (p.index + 1) % len(p.cards)
checked++
// Skip expired cards
if p.ttl > 0 && now.Sub(entry.addedAt) > p.ttl {
continue
}
// Skip rejected cards
if entry.rejected {
continue
}
// Check bind limits
if !p.multiBind && entry.bindCount > 0 {
continue
}
if p.multiBind && p.maxBinds > 0 && entry.bindCount >= p.maxBinds {
continue
}
// Don't increment bindCount here — caller must call IncrementBind() after
// a real payment attempt reaches a terminal state (success or upstream decline).
log.Printf("[card-pool] dispensing card %s...%s (current binds=%d)",
entry.card.Number[:4], entry.card.Number[len(entry.card.Number)-4:], entry.bindCount)
return entry.card, nil
}
return nil, fmt.Errorf("no available cards in pool (total=%d)", len(p.cards))
}
// ReportResult handles payment terminal states:
// - success=true: bindCount++ (card used successfully)
// - success=false: bindCount++ AND rejected=true (upstream declined, card is dead)
func (p *CardPool) ReportResult(card *CardInfo, success bool) {
p.mu.Lock()
defer p.mu.Unlock()
for _, entry := range p.cards {
if entry.card.Number == card.Number {
entry.bindCount++
if !success {
entry.rejected = true
log.Printf("[card-pool] card %s...%s marked as rejected (bind #%d)",
card.Number[:4], card.Number[len(card.Number)-4:], entry.bindCount)
} else {
log.Printf("[card-pool] card %s...%s bind #%d recorded (success)",
card.Number[:4], card.Number[len(card.Number)-4:], entry.bindCount)
}
return
}
}
}
// Stats returns pool statistics.
func (p *CardPool) Stats() (total, available, expired, rejected int) {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
total = len(p.cards)
for _, e := range p.cards {
switch {
case e.rejected:
rejected++
case p.ttl > 0 && now.Sub(e.addedAt) > p.ttl:
expired++
case !p.multiBind && e.bindCount > 0:
// used up in single-bind mode
case p.multiBind && p.maxBinds > 0 && e.bindCount >= p.maxBinds:
// used up max binds
default:
available++
}
}
return
}