143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// StarsStore 是 store.StarsStore 的内存实现,复刻 postgres 版的原子语义
|
||
// (在单个互斥锁下完成读-检查-写,等价于 SELECT ... FOR UPDATE)。
|
||
type StarsStore struct {
|
||
mu sync.Mutex
|
||
states map[int64]*starsState
|
||
nextID int64
|
||
}
|
||
|
||
type starsState struct {
|
||
balance int64
|
||
granted bool
|
||
txns []domain.StarsTransaction // 追加序,读时倒序
|
||
}
|
||
|
||
// NewStarsStore 创建内存 StarsStore。
|
||
func NewStarsStore() *StarsStore {
|
||
return &StarsStore{states: make(map[int64]*starsState)}
|
||
}
|
||
|
||
func (s *StarsStore) GetBalance(_ context.Context, userID int64) (domain.StarsBalance, error) {
|
||
if userID == 0 {
|
||
return domain.StarsBalance{}, nil
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
st := s.states[userID]
|
||
if st == nil {
|
||
return domain.StarsBalance{UserID: userID}, nil
|
||
}
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: st.granted}, nil
|
||
}
|
||
|
||
func (s *StarsStore) EnsureGrant(_ context.Context, userID, amount int64, date int) (domain.StarsBalance, bool, error) {
|
||
if userID == 0 {
|
||
return domain.StarsBalance{}, false, nil
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
st := s.states[userID]
|
||
if st == nil {
|
||
st = &starsState{}
|
||
s.states[userID] = st
|
||
}
|
||
if amount <= 0 {
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: st.granted}, false, nil
|
||
}
|
||
if st.granted {
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: true}, false, nil
|
||
}
|
||
st.balance += amount
|
||
st.granted = true
|
||
s.appendTxn(st, userID, amount, domain.StarsReasonGrant, domain.Peer{}, date, "", "")
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: true}, true, nil
|
||
}
|
||
|
||
func (s *StarsStore) Credit(_ 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
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
st := s.states[userID]
|
||
if st == nil {
|
||
st = &starsState{}
|
||
s.states[userID] = st
|
||
}
|
||
st.balance += amount
|
||
s.appendTxn(st, userID, amount, reason, peer, date, title, desc)
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: st.granted}, nil
|
||
}
|
||
|
||
func (s *StarsStore) Debit(_ 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
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
st := s.states[userID]
|
||
if st == nil || st.balance < amount {
|
||
return domain.StarsBalance{}, domain.ErrStarsInsufficient
|
||
}
|
||
st.balance -= amount
|
||
s.appendTxn(st, userID, -amount, reason, peer, date, title, desc)
|
||
return domain.StarsBalance{UserID: userID, Balance: st.balance, Granted: st.granted}, nil
|
||
}
|
||
|
||
func (s *StarsStore) ListTransactions(_ 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
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
st := s.states[userID]
|
||
if st == nil {
|
||
return domain.StarsTransactionPage{}, nil
|
||
}
|
||
page := domain.StarsTransactionPage{Balance: st.balance}
|
||
cursor, hasCursor := domain.DecodeStarsCursor(offset)
|
||
// 倒序遍历(id DESC)。
|
||
out := make([]domain.StarsTransaction, 0, limit)
|
||
for i := len(st.txns) - 1; i >= 0; i-- {
|
||
t := st.txns[i]
|
||
if hasCursor && t.ID >= cursor {
|
||
continue
|
||
}
|
||
out = append(out, t)
|
||
if len(out) == limit {
|
||
// 还有更早的流水则给出下一页游标。
|
||
if i-1 >= 0 {
|
||
page.NextOffset = domain.EncodeStarsCursor(t.ID)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
page.Transactions = out
|
||
return page, nil
|
||
}
|
||
|
||
func (s *StarsStore) appendTxn(st *starsState, userID, amount int64, reason domain.StarsTransactionReason, peer domain.Peer, date int, title, desc string) {
|
||
s.nextID++
|
||
st.txns = append(st.txns, domain.StarsTransaction{
|
||
ID: s.nextID,
|
||
UserID: userID,
|
||
Peer: peer,
|
||
Amount: amount,
|
||
Date: date,
|
||
Reason: reason,
|
||
Title: title,
|
||
Description: desc,
|
||
})
|
||
}
|