package wallet import ( "context" "crypto/rand" "fmt" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/timeutil" "hfb_sys/backend/pkg/money" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } type Entry struct { UserID uint64 OrderID *uint64 Direction string AmountCent int64 BalanceType string BizType string BizNo string Remark string } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) Account(ctx context.Context, userID uint64) (*AccountDTO, error) { var account model.WalletAccount err := r.db.WithContext(ctx).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(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) { var total int64 if err := r.db.WithContext(ctx).Model(&model.WalletLedger{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []model.WalletLedger if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil { return nil, err } items := make([]LedgerDTO, 0, len(rows)) for _, row := range rows { items = append(items, toLedgerDTO(row)) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } // Withdraw 保留仓库能力;当前公开提现入口在 service 层标记为待开发,不会调用到这里。 func (r *Repository) Withdraw(ctx context.Context, userID uint64, amountCent int64) (*AccountDTO, error) { if userID == 0 || amountCent <= 0 { return nil, ErrInvalidAmount } err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, Direction: "out", AmountCent: amountCent, BalanceType: "available", BizType: "withdraw_apply", BizNo: fmt.Sprintf("WD%d", time.Now().UnixNano()), Remark: "卖家申请提现", }) }) if err != nil { return nil, err } return r.Account(ctx, userID) } func (r *Repository) AdminLedger(ctx context.Context, query AdminLedgerQuery) (*PaginatedResult, error) { db := r.db.WithContext(ctx).Table("wallet_ledger AS wl"). Select(`wl.id, wl.ledger_no, wl.user_id, COALESCE(u.phone, '') AS user_phone, COALESCE(u.nickname, '') AS user_nickname, wl.order_id, COALESCE(ro.order_no, '') AS order_no, wl.direction, wl.amount_cent, wl.balance_after_cent, wl.balance_type, wl.biz_type, wl.biz_no, wl.remark, wl.created_at`). Joins("LEFT JOIN users AS u ON u.id = wl.user_id"). Joins("LEFT JOIN rental_orders AS ro ON ro.id = wl.order_id") countDB := r.db.WithContext(ctx).Model(&model.WalletLedger{}) if query.UserID > 0 { db = db.Where("wl.user_id = ?", query.UserID) countDB = countDB.Where("user_id = ?", query.UserID) } if query.OrderID > 0 { db = db.Where("wl.order_id = ?", query.OrderID) countDB = countDB.Where("order_id = ?", query.OrderID) } if query.BizType != "" { db = db.Where("wl.biz_type = ?", query.BizType) countDB = countDB.Where("biz_type = ?", query.BizType) } var total int64 if err := countDB.Count(&total).Error; err != nil { return nil, err } offset := (query.Page - 1) * query.PageSize items := make([]AdminLedgerDTO, 0, query.PageSize) if err := db.Order("wl.id DESC").Offset(offset).Limit(query.PageSize).Scan(&items).Error; err != nil { return nil, err } return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil } func AppendEntries(tx *gorm.DB, entries ...Entry) error { for _, entry := range entries { if entry.AmountCent <= 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 } balanceAfterCent, 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, AmountCent: entry.AmountCent, BalanceAfterCent: balanceAfterCent, 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, AvailableBalanceCent: 0, FrozenBalanceCent: 0, Status: "active", } return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&account).Error } func applyEntry(account *model.WalletAccount, entry Entry) (int64, error) { switch entry.BalanceType { case "available": if entry.Direction == "in" { account.AvailableBalanceCent += entry.AmountCent } else { if account.AvailableBalanceCent < entry.AmountCent { return 0, ErrInsufficientBalance } account.AvailableBalanceCent -= entry.AmountCent } return account.AvailableBalanceCent, nil case "frozen": if entry.Direction == "in" { account.FrozenBalanceCent += entry.AmountCent } else { if account.FrozenBalanceCent < entry.AmountCent { return 0, ErrInsufficientBalance } account.FrozenBalanceCent -= entry.AmountCent } return account.FrozenBalanceCent, nil default: return 0, fmt.Errorf("unknown balance type: %s", entry.BalanceType) } } func roundWalletMoney(yuan float64) float64 { return money.Round(yuan) } func toAccountDTO(account model.WalletAccount) *AccountDTO { return &AccountDTO{ UserID: account.UserID, AvailableBalanceCent: account.AvailableBalanceCent, FrozenBalanceCent: account.FrozenBalanceCent, Status: account.Status, } } func toLedgerDTO(ledger model.WalletLedger) LedgerDTO { return LedgerDTO{ ID: ledger.ID, LedgerNo: ledger.LedgerNo, UserID: ledger.UserID, OrderID: ledger.OrderID, Direction: ledger.Direction, AmountCent: ledger.AmountCent, BalanceAfterCent: ledger.BalanceAfterCent, BalanceType: ledger.BalanceType, BizType: ledger.BizType, BizNo: ledger.BizNo, Remark: ledger.Remark, CreatedAt: ledger.CreatedAt, } } func newLedgerNo() (string, error) { now := timeutil.ShanghaiNow() prefix := "WL" + now.Format("20060102150405") randomBytes := make([]byte, 4) if _, err := rand.Read(randomBytes); err != nil { return "", err } suffix := fmt.Sprintf("%08d", uint32(randomBytes[0])<<24|uint32(randomBytes[1])<<16|uint32(randomBytes[2])<<8|uint32(randomBytes[3])) return prefix + suffix[:7], nil }