用户管理增加余额调账功能
支持后台加减可用余额并写入资金流水与审计,优化操作列按钮样式。
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user