用户管理增加余额调账功能

支持后台加减可用余额并写入资金流水与审计,优化操作列按钮样式。
This commit is contained in:
yml2213
2026-07-13 21:30:38 +08:00
parent a2a9ade68e
commit b96d6141f5
10 changed files with 642 additions and 57 deletions
@@ -3,12 +3,14 @@ package adminuser
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/wallet"
"hfb_sys/backend/pkg/crypto"
"gorm.io/gorm"
@@ -43,11 +45,14 @@ func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQue
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count,
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`).
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent,
COALESCE(w.available_balance_cent, 0) AS available_balance_cent,
COALESCE(w.frozen_balance_cent, 0) AS frozen_balance_cent`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id")
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Joins("LEFT JOIN wallet_accounts AS w ON w.user_id = u.id")
listTx = applyUserFilter(listTx, query)
err := listTx.
Order("u.id DESC").
@@ -270,11 +275,14 @@ func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error)
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count,
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`).
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent,
COALESCE(w.available_balance_cent, 0) AS available_balance_cent,
COALESCE(w.frozen_balance_cent, 0) AS frozen_balance_cent`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Joins("LEFT JOIN wallet_accounts AS w ON w.user_id = u.id").
Where("u.id = ?", userID).
First(&row).Error
if err != nil {
@@ -284,12 +292,96 @@ func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error)
return &dto, nil
}
// AdjustWallet 人工调整用户可用余额:写 wallet_ledger + 审计日志,同事务提交。
func (r *Repository) AdjustWallet(ctx context.Context, adminID uint64, userID uint64, req WalletAdjustRequest, meta AuditMeta) (*UserDTO, error) {
direction := req.Direction
amountCent := req.AmountCent
reason := req.Reason
referenceNo := req.ReferenceNo
bizType := "admin_credit"
remarkPrefix := "人工加款"
if direction == "out" {
bizType = "admin_debit"
remarkPrefix = "人工扣款"
}
bizNo := fmt.Sprintf("ADJ%d%d", adminID, time.Now().UnixNano())
remark := remarkPrefix + ": " + reason
if referenceNo != "" {
remark = remark + " [" + referenceNo + "]"
}
if len([]rune(remark)) > 255 {
runes := []rune(remark)
remark = string(runes[:255])
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err
}
// 确保钱包账户存在并锁定,读取调账前余额
if err := wallet.EnsureAccountTx(tx, userID); err != nil {
return err
}
var account model.WalletAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id = ?", userID).First(&account).Error; err != nil {
return err
}
beforeAvailable := account.AvailableBalanceCent
beforeFrozen := account.FrozenBalanceCent
if err := wallet.AppendEntries(tx, wallet.Entry{
UserID: userID,
Direction: direction,
AmountCent: amountCent,
BalanceType: "available",
BizType: bizType,
BizNo: bizNo,
Remark: remark,
}); err != nil {
if errors.Is(err, wallet.ErrInsufficientBalance) {
return ErrInsufficientBalance
}
if errors.Is(err, wallet.ErrInvalidAmount) {
return ErrInvalidWalletAmount
}
return err
}
if err := tx.Where("user_id = ?", userID).First(&account).Error; err != nil {
return err
}
return appendAuditLog(tx, adminID, "admin_user.wallet_adjust", user.ID, meta, map[string]any{
"user_id": user.ID,
"direction": direction,
"amount_cent": amountCent,
"before_available_cent": beforeAvailable,
"after_available_cent": account.AvailableBalanceCent,
"before_frozen_cent": beforeFrozen,
"after_frozen_cent": account.FrozenBalanceCent,
"reason": reason,
"reference_no": referenceNo,
"biz_type": bizType,
"biz_no": bizNo,
})
})
if err != nil {
return nil, err
}
return r.Find(ctx, userID)
}
type userRow struct {
model.User
OrderCount int64
ListingCount int64
DisputeCount int64
DepositFreeUsedCent int64
OrderCount int64
ListingCount int64
DisputeCount int64
DepositFreeUsedCent int64
AvailableBalanceCent int64
FrozenBalanceCent int64
}
func (row userRow) toDTO() UserDTO {
@@ -307,6 +399,8 @@ func (row userRow) toDTO() UserDTO {
DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemainingCent: remaining,
AvailableBalanceCent: row.AvailableBalanceCent,
FrozenBalanceCent: row.FrozenBalanceCent,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,