package wallet import ( "crypto/rand" "encoding/hex" "fmt" "time" "hfb_sys/backend/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } type Entry struct { UserID uint64 OrderID *uint64 Direction string Amount float64 BalanceType string BizType string BizNo string Remark string } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) Account(userID uint64) (*AccountDTO, error) { var account model.WalletAccount err := r.db.Transaction(func(tx *gorm.DB) error { if err := ensureAccount(tx, userID); err != nil { return err } return tx.Where("user_id = ?", userID).First(&account).Error }) if err != nil { return nil, err } return toAccountDTO(account), nil } func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) { var rows []model.WalletLedger if err := r.db.Where("user_id = ?", userID).Order("id DESC").Limit(100).Find(&rows).Error; err != nil { return nil, err } items := make([]LedgerDTO, 0, len(rows)) for _, row := range rows { items = append(items, toLedgerDTO(row)) } return items, nil } func AppendEntries(tx *gorm.DB, entries ...Entry) error { for _, entry := range entries { if entry.Amount <= 0 { continue } if err := ensureAccount(tx, entry.UserID); err != nil { return err } var account model.WalletAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id = ?", entry.UserID).First(&account).Error; err != nil { return err } balanceAfter, err := applyEntry(&account, entry) if err != nil { return err } if err := tx.Save(&account).Error; err != nil { return err } ledgerNo, err := newLedgerNo() if err != nil { return err } ledger := model.WalletLedger{ LedgerNo: ledgerNo, UserID: entry.UserID, OrderID: entry.OrderID, Direction: entry.Direction, Amount: entry.Amount, BalanceAfter: balanceAfter, BalanceType: entry.BalanceType, BizType: entry.BizType, BizNo: entry.BizNo, Remark: entry.Remark, } if err := tx.Create(&ledger).Error; err != nil { return err } } return nil } func ensureAccount(tx *gorm.DB, userID uint64) error { account := model.WalletAccount{ UserID: userID, AvailableBalance: 0, FrozenBalance: 0, Status: "active", } return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&account).Error } func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) { switch entry.BalanceType { case "available": if entry.Direction == "in" { account.AvailableBalance += entry.Amount } else { account.AvailableBalance -= entry.Amount } return account.AvailableBalance, nil case "frozen": if entry.Direction == "in" { account.FrozenBalance += entry.Amount } else { account.FrozenBalance -= entry.Amount } return account.FrozenBalance, nil default: return 0, fmt.Errorf("unsupported balance type: %s", entry.BalanceType) } } func toAccountDTO(account model.WalletAccount) *AccountDTO { return &AccountDTO{ UserID: account.UserID, AvailableBalance: account.AvailableBalance, FrozenBalance: account.FrozenBalance, Status: account.Status, } } func toLedgerDTO(row model.WalletLedger) LedgerDTO { return LedgerDTO{ ID: row.ID, LedgerNo: row.LedgerNo, UserID: row.UserID, OrderID: row.OrderID, Direction: row.Direction, Amount: row.Amount, BalanceAfter: row.BalanceAfter, BalanceType: row.BalanceType, BizType: row.BizType, BizNo: row.BizNo, Remark: row.Remark, CreatedAt: row.CreatedAt, } } func newLedgerNo() (string, error) { buf := make([]byte, 4) if _, err := rand.Read(buf); err != nil { return "", err } return fmt.Sprintf("WL%d%s", time.Now().UnixNano(), hex.EncodeToString(buf)), nil }