improve(wallet):优化管理后台的钱包功能
This commit is contained in:
1 parent
e464da852c
commit
2e0119ee85
7 files changed
+250
-24
No files matched your search
@@ -86,6 +86,7 @@ type IAdminV1 interface {
|
||||
AdminWalletList(ctx context.Context, req *v1.AdminWalletListReq) (res *v1.AdminWalletListRes, err error)
|
||||
AdminWalletInfo(ctx context.Context, req *v1.AdminWalletInfoReq) (res *v1.AdminWalletInfoRes, err error)
|
||||
AdminWalletAdjust(ctx context.Context, req *v1.AdminWalletAdjustReq) (res *v1.AdminWalletAdjustRes, err error)
|
||||
AdminWalletOfflineRecharge(ctx context.Context, req *v1.AdminWalletOfflineRechargeReq) (res *v1.AdminWalletOfflineRechargeRes, err error)
|
||||
AdminWalletTransactionList(ctx context.Context, req *v1.AdminWalletTransactionListReq) (res *v1.AdminWalletTransactionListRes, err error)
|
||||
AdminWalletSetWarningThreshold(ctx context.Context, req *v1.AdminWalletSetWarningThresholdReq) (res *v1.AdminWalletSetWarningThresholdRes, err error)
|
||||
AdminUsageLogExport(ctx context.Context, req *v1.AdminUsageLogExportReq) (res *v1.AdminUsageLogExportRes, err error)
|
||||
|
||||
@@ -282,6 +282,20 @@ type AdminWalletAdjustReq struct {
|
||||
|
||||
type AdminWalletAdjustRes struct{}
|
||||
|
||||
type AdminWalletOfflineRechargeReq struct {
|
||||
g.Meta `path:"/wallets/{tenant_id}/offline-recharge" method:"post" mime:"json" tags:"管理后台-钱包" summary:"线下充值入账"`
|
||||
TenantID int64 `json:"tenant_id" in:"path" v:"required|min:1" dc:"租户ID"`
|
||||
Amount float64 `json:"amount" v:"required|min:0.01" dc:"线下入账金额(人民币 CNY),按平台汇率换算为 USD 到账"`
|
||||
TransactionNo string `json:"transaction_no" dc:"银行转账流水号(选填,用于对账与防重复入账)"`
|
||||
Description string `json:"description" v:"required" dc:"入账说明(必填,如客户名称、转账原因)"`
|
||||
}
|
||||
|
||||
type AdminWalletOfflineRechargeRes struct {
|
||||
CreditedUSD float64 `json:"credited_usd" dc:"实际入账美元金额"`
|
||||
Rate float64 `json:"rate" dc:"使用的 CNY→USD 汇率"`
|
||||
Balance float64 `json:"balance" dc:"入账后总余额(USD)"`
|
||||
}
|
||||
|
||||
type AdminWalletTransactionListReq struct {
|
||||
g.Meta `path:"/wallets/{tenant_id}/transactions" method:"get" mime:"json" tags:"管理后台-钱包" summary:"交易流水"`
|
||||
TenantID int64 `json:"tenant_id" in:"path" v:"required|min:1" dc:"租户ID"`
|
||||
|
||||
@@ -58,3 +58,6 @@ func (c *ControllerV1) AdminTransactionList(ctx context.Context, req *v1.AdminTr
|
||||
func (c *ControllerV1) AdminDashboardModelHourly(ctx context.Context, req *v1.AdminDashboardModelHourlyReq) (res *v1.AdminDashboardModelHourlyRes, err error) {
|
||||
return service.Admin().GetModelHourlyCost(ctx, req)
|
||||
}
|
||||
func (c *ControllerV1) AdminWalletOfflineRecharge(ctx context.Context, req *v1.AdminWalletOfflineRechargeReq) (res *v1.AdminWalletOfflineRechargeRes, err error) {
|
||||
return service.Admin().OfflineRecharge(ctx, req)
|
||||
}
|
||||
@@ -603,6 +603,123 @@ func (s *sAdmin) AdjustBalance(ctx context.Context, req *v1.AdminWalletAdjustReq
|
||||
return &v1.AdminWalletAdjustRes{}, nil
|
||||
}
|
||||
|
||||
// OfflineRecharge 线下充值入账(管理后台)
|
||||
// 场景:用户线下银行转账(人民币 CNY),运营确认到账后按平台汇率换算为 USD 入账。
|
||||
// 与 AdjustBalance 的区别:走正规充值链路——余额与累计充值同步累加、触发等级检查、
|
||||
// 流水类型为 recharge,并在描述中携带 CNY 快照(原始人民币 + 汇率 + 入账 USD + 转账流水号),
|
||||
// 供现金对账与开票追溯。
|
||||
func (s *sAdmin) OfflineRecharge(ctx context.Context, req *v1.AdminWalletOfflineRechargeReq) (*v1.AdminWalletOfflineRechargeRes, error) {
|
||||
tenantID := req.TenantID
|
||||
cnyAmount := billing.NewFromFloat(req.Amount)
|
||||
if !cnyAmount.IsPositive() {
|
||||
return nil, common.NewBadRequestError("入账金额必须大于 0")
|
||||
}
|
||||
|
||||
// 唯一换汇点:CNY→USD 只经 billing.ConvertCNYToUSD(与充值履约 FulfillOrder 同一函数),
|
||||
// 汇率取一次用于入账与快照,保证换算可重建。
|
||||
rate := billing.GetExchangeRateCNYToUSD(ctx)
|
||||
usdAmount := billing.ConvertCNYToUSD(ctx, req.Amount)
|
||||
if usdAmount.IsZero() {
|
||||
return nil, common.NewBadRequestError("按当前汇率换算后到账金额为 0,请检查汇率配置")
|
||||
}
|
||||
|
||||
// 转账流水号软去重(防重复入账):同一租户下已入账过该流水号则拒绝。
|
||||
// 非强原子,配合前端 loading 禁用按钮,拦截人工重复提交足够。
|
||||
if req.TransactionNo != "" {
|
||||
count, err := dao.BilTransactions.Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("type", "recharge").
|
||||
Where("description like ?", "%转账流水号 "+req.TransactionNo).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, common.NewBadRequestError("该转账流水号已入账,请勿重复操作")
|
||||
}
|
||||
}
|
||||
|
||||
var creditedUsd decimal.Decimal
|
||||
var balanceAfter decimal.Decimal
|
||||
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 钱包存在性校验
|
||||
var wallet *struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := dao.BilWallets.Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Fields("id").
|
||||
Scan(&wallet); err != nil {
|
||||
return err
|
||||
}
|
||||
if wallet == nil {
|
||||
return common.NewBadRequestError("钱包不存在")
|
||||
}
|
||||
|
||||
// 原子入账:余额与累计充值同步累加(与 creditWalletTx 一致),
|
||||
// 单条 UPDATE 避免 read-modify-write 竞态,并发下不丢更新。
|
||||
if _, err := g.DB().Ctx(ctx).Exec(ctx,
|
||||
"UPDATE bil_wallets SET balance = balance + ?, cumulative_recharge = cumulative_recharge + ?, updated_at = NOW() WHERE id = ?",
|
||||
usdAmount, usdAmount, wallet.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读回入账后余额,用于流水快照
|
||||
var bal struct {
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
FrozenBalance decimal.Decimal `json:"frozen_balance"`
|
||||
}
|
||||
if err := dao.BilWallets.Ctx(ctx).
|
||||
Where("id", wallet.ID).
|
||||
Fields("balance, frozen_balance").
|
||||
Scan(&bal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 流水(type=recharge):描述拼装 CNY 快照 + 转账流水号,供现金对账与开票追溯
|
||||
desc := fmt.Sprintf("线下充值入账 CNY %.2f × 汇率 %.6f = USD %s", cnyAmount.InexactFloat64(), rate, usdAmount)
|
||||
if req.Description != "" {
|
||||
desc = req.Description + ";" + desc
|
||||
}
|
||||
if req.TransactionNo != "" {
|
||||
desc += ";转账流水号 " + req.TransactionNo
|
||||
}
|
||||
if _, err := dao.BilTransactions.Ctx(ctx).Insert(do.BilTransactions{
|
||||
TenantId: tenantID,
|
||||
WalletId: wallet.ID,
|
||||
Type: "recharge",
|
||||
Amount: usdAmount,
|
||||
BalanceAfter: bal.Balance,
|
||||
FrozenAfter: bal.FrozenBalance,
|
||||
Description: desc,
|
||||
}); err != nil {
|
||||
return gerror.Wrapf(err, "record offline recharge transaction")
|
||||
}
|
||||
|
||||
// 充值后检查租户等级(仅升不降)并重置低余额预警标记
|
||||
if err := billing.CheckAndUpgradeLevel(ctx, tenantID); err != nil {
|
||||
return gerror.Wrapf(err, "check upgrade level after offline recharge")
|
||||
}
|
||||
billing.ResetLowBalanceNotified(ctx, tenantID)
|
||||
|
||||
creditedUsd = usdAmount
|
||||
balanceAfter = bal.Balance
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 事务提交后清除钱包两级缓存(进程内 + Redis),避免 300s TTL 内读到旧余额
|
||||
billing.InvalidateWallet(ctx, tenantID)
|
||||
|
||||
return &v1.AdminWalletOfflineRechargeRes{
|
||||
CreditedUSD: billing.InexactFloat64(creditedUsd),
|
||||
Rate: rate,
|
||||
Balance: billing.InexactFloat64(balanceAfter),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWalletInfo 获取租户钱包信息(管理后台)
|
||||
func (s *sAdmin) GetWalletInfo(ctx context.Context, req *v1.AdminWalletInfoReq) (*v1.AdminWalletInfoRes, error) {
|
||||
type walletRow struct {
|
||||
@@ -657,6 +774,27 @@ func (s *sAdmin) GetWalletTransactions(ctx context.Context, req *v1.AdminWalletT
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 批量关联用户名:user_id → tnt_users.username(consume 类型为实际消费用户,其余类型一般为空)。
|
||||
// 复用审计模块的批量查询工具,一次 IN 查询解决,避免逐行 N+1。
|
||||
if len(records) > 0 {
|
||||
userKeys := make([]string, 0, len(records))
|
||||
seen := make(map[int64]bool, len(records))
|
||||
for _, r := range records {
|
||||
if r.UserId > 0 && !seen[r.UserId] {
|
||||
seen[r.UserId] = true
|
||||
userKeys = append(userKeys, fmt.Sprintf("%d:%d", req.TenantID, r.UserId))
|
||||
}
|
||||
}
|
||||
if len(userKeys) > 0 {
|
||||
userMap := common.BatchQueryUserNames(ctx, userKeys)
|
||||
for _, r := range records {
|
||||
if r.UserId > 0 {
|
||||
r.Username = userMap[fmt.Sprintf("%d:%d", req.TenantID, r.UserId)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &v1.AdminWalletTransactionListRes{
|
||||
List: records,
|
||||
Total: total,
|
||||
|
||||
@@ -134,6 +134,7 @@ var adminPermissionRules = []permissionRule{
|
||||
{method: "GET", path: "/api/admin/wallets", perm: "billing:view"},
|
||||
{method: "GET", prefix: "/api/admin/wallets/", perm: "billing:view"},
|
||||
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/adjust", perm: "billing:refund"},
|
||||
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/offline-recharge", perm: "billing:refund"},
|
||||
{method: "PUT", prefix: "/api/admin/wallets/", suffix: "/warning-threshold", perm: "billing:view"},
|
||||
|
||||
// ── plan 套餐管理 ──
|
||||
|
||||
@@ -162,6 +162,12 @@ type (
|
||||
GetTenantWallets(ctx context.Context, req *v1.AdminWalletListReq) (*v1.AdminWalletListRes, error)
|
||||
// AdjustBalance 调整租户余额(管理后台)
|
||||
AdjustBalance(ctx context.Context, req *v1.AdminWalletAdjustReq) (*v1.AdminWalletAdjustRes, error)
|
||||
// OfflineRecharge 线下充值入账(管理后台)
|
||||
// 场景:用户线下银行转账(人民币 CNY),运营确认到账后按平台汇率换算为 USD 入账。
|
||||
// 与 AdjustBalance 的区别:走正规充值链路——余额与累计充值同步累加、触发等级检查、
|
||||
// 流水类型为 recharge,并在描述中携带 CNY 快照(原始人民币 + 汇率 + 入账 USD + 转账流水号),
|
||||
// 供现金对账与开票追溯。
|
||||
OfflineRecharge(ctx context.Context, req *v1.AdminWalletOfflineRechargeReq) (*v1.AdminWalletOfflineRechargeRes, error)
|
||||
// GetWalletInfo 获取租户钱包信息(管理后台)
|
||||
GetWalletInfo(ctx context.Context, req *v1.AdminWalletInfoReq) (*v1.AdminWalletInfoRes, error)
|
||||
// GetWalletTransactions 获取租户钱包交易流水(管理后台)
|
||||
|
||||
@@ -448,6 +448,9 @@ const txFilterType = ref('')
|
||||
const showRechargeModal = ref(false)
|
||||
const rechargeForm = reactive({ amount: 0, description: '' })
|
||||
const rechargeLoading = ref(false)
|
||||
const showOfflineRechargeModal = ref(false)
|
||||
const offlineRechargeForm = reactive({ amount: 0, transaction_no: '', description: '' })
|
||||
const offlineRechargeLoading = ref(false)
|
||||
const showThresholdModal = ref(false)
|
||||
const thresholdForm = reactive({ threshold: 0 })
|
||||
const thresholdLoading = ref(false)
|
||||
@@ -470,19 +473,22 @@ const txColumns: TableColumnData[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '金额', dataIndex: 'amount', width: 120,
|
||||
title: '金额', dataIndex: 'amount', width: 140,
|
||||
render({ record }) {
|
||||
const val = parseFloat(record.amount) || 0
|
||||
const color = val >= 0 ? 'rgb(var(--green-6))' : 'rgb(var(--red-6))'
|
||||
const prefix = val >= 0 ? '+' : ''
|
||||
return h('span', { style: { fontWeight: 600, color } }, `${prefix}${val.toFixed(4)}`)
|
||||
return h('span', { style: { fontWeight: 600, color } }, `${prefix}${val.toFixed(6)}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '变动后余额', dataIndex: 'balance_after', width: 120,
|
||||
render({ record }) { return `$${(parseFloat(record.balance_after) || 0).toFixed(2)}` },
|
||||
title: '变动后余额', dataIndex: 'balance_after', width: 140,
|
||||
render({ record }) { return `$${(parseFloat(record.balance_after) || 0).toFixed(6)}` },
|
||||
},
|
||||
{
|
||||
title: '用户', dataIndex: 'username', width: 120,
|
||||
render({ record }) { return record.username || '--' },
|
||||
},
|
||||
{ title: '用户', dataIndex: 'username', width: 120 },
|
||||
{ title: '请求ID', dataIndex: 'request_id', width: 140, ellipsis: true },
|
||||
{ title: '模型', dataIndex: 'model_name', width: 130 },
|
||||
{ title: '描述', dataIndex: 'description', ellipsis: true },
|
||||
@@ -528,9 +534,9 @@ async function handleRecharge(done: (closed: boolean) => void) {
|
||||
try {
|
||||
await request.post(`/admin/wallets/${tenantId}/adjust`, {
|
||||
amount: rechargeForm.amount,
|
||||
description: rechargeForm.description || (rechargeForm.amount > 0 ? '管理员充值' : '管理员扣减'),
|
||||
description: rechargeForm.description || (rechargeForm.amount > 0 ? '管理员增加余额' : '管理员扣减余额'),
|
||||
})
|
||||
Message.success(rechargeForm.amount > 0 ? '充值成功' : '扣减成功')
|
||||
Message.success(rechargeForm.amount > 0 ? '余额已增加' : '余额已扣减')
|
||||
done(true)
|
||||
await fetchWalletDetail()
|
||||
await fetchDetail()
|
||||
@@ -541,6 +547,35 @@ async function handleRecharge(done: (closed: boolean) => void) {
|
||||
}
|
||||
}
|
||||
|
||||
function openOfflineRecharge() {
|
||||
offlineRechargeForm.amount = 0
|
||||
offlineRechargeForm.transaction_no = ''
|
||||
offlineRechargeForm.description = ''
|
||||
showOfflineRechargeModal.value = true
|
||||
}
|
||||
|
||||
async function handleOfflineRecharge(done: (closed: boolean) => void) {
|
||||
if (!offlineRechargeForm.amount || offlineRechargeForm.amount <= 0) { Message.warning('请输入大于 0 的入账金额'); return }
|
||||
if (!offlineRechargeForm.description.trim()) { Message.warning('请填写入账说明'); return }
|
||||
offlineRechargeLoading.value = true
|
||||
try {
|
||||
const res: any = await request.post(`/admin/wallets/${tenantId}/offline-recharge`, {
|
||||
amount: offlineRechargeForm.amount,
|
||||
transaction_no: offlineRechargeForm.transaction_no || undefined,
|
||||
description: offlineRechargeForm.description,
|
||||
})
|
||||
const data = res.data?.data || {}
|
||||
Message.success(`入账成功,到账 $${parseFloat(data.credited_usd || 0).toFixed(2)}(汇率 ${data.rate})`)
|
||||
done(true)
|
||||
await fetchWalletDetail()
|
||||
await fetchDetail()
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
offlineRechargeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openThreshold() {
|
||||
thresholdForm.threshold = walletInfo.value?.warning_threshold ? parseFloat(walletInfo.value.warning_threshold) : 0
|
||||
showThresholdModal.value = true
|
||||
@@ -727,22 +762,23 @@ onMounted(() => {
|
||||
<ACard :bordered="false" title="钱包信息" class="mb-4">
|
||||
<template #extra>
|
||||
<ASpace>
|
||||
<AButton type="primary" @click="openRecharge">充值 / 调整</AButton>
|
||||
<AButton type="primary" @click="openRecharge">调整余额</AButton>
|
||||
<AButton status="success" @click="openOfflineRecharge">线下入账</AButton>
|
||||
<AButton @click="openThreshold">预警设置</AButton>
|
||||
</ASpace>
|
||||
</template>
|
||||
<ADescriptions :column="3" bordered size="medium">
|
||||
<ADescriptionsItem label="可用余额">
|
||||
<span class="money">${{ (walletInfo.balance - walletInfo.frozen_balance).toFixed(2) }}</span>
|
||||
<span class="money">${{ (walletInfo.balance - walletInfo.frozen_balance).toFixed(6) }}</span>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem label="总余额">
|
||||
${{ parseFloat(walletInfo.balance).toFixed(2) }}
|
||||
${{ parseFloat(walletInfo.balance).toFixed(6) }}
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem label="冻结余额">
|
||||
<span style="color: rgb(var(--orange-6))">${{ parseFloat(walletInfo.frozen_balance).toFixed(2) }}</span>
|
||||
<span style="color: rgb(var(--orange-6))">${{ parseFloat(walletInfo.frozen_balance).toFixed(6) }}</span>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem label="预警阈值">
|
||||
{{ walletInfo.warning_threshold > 0 ? `$${parseFloat(walletInfo.warning_threshold).toFixed(2)}` : '关闭' }}
|
||||
{{ walletInfo.warning_threshold > 0 ? `$${parseFloat(walletInfo.warning_threshold).toFixed(6)}` : '关闭' }}
|
||||
</ADescriptionsItem>
|
||||
</ADescriptions>
|
||||
</ACard>
|
||||
@@ -750,7 +786,7 @@ onMounted(() => {
|
||||
<!-- Transaction History -->
|
||||
<ACard :bordered="false" title="交易记录">
|
||||
<template #extra>
|
||||
<ARadioGroup v-model="txFilterType" type="button" size="small" @change="() => { txPagination.current = 1; fetchWalletDetail() }">
|
||||
<ARadioGroup v-model="txFilterType" type="button" size="small" @change="() => { txPagination.current = 1; fetchWalletTransactions() }">
|
||||
<ARadio value="">全部</ARadio>
|
||||
<ARadio value="recharge">充值</ARadio>
|
||||
<ARadio value="redemption">兑换码</ARadio>
|
||||
@@ -773,8 +809,8 @@ onMounted(() => {
|
||||
:page-size-options="[10, 20, 50]"
|
||||
show-page-size
|
||||
show-jumper
|
||||
@change="fetchWalletDetail"
|
||||
@page-size-change="() => { txPagination.current = 1; fetchWalletDetail() }"
|
||||
@change="fetchWalletTransactions"
|
||||
@page-size-change="() => { txPagination.current = 1; fetchWalletTransactions() }"
|
||||
/>
|
||||
</div>
|
||||
</ACard>
|
||||
@@ -964,23 +1000,50 @@ onMounted(() => {
|
||||
</template>
|
||||
</ADrawer>
|
||||
|
||||
<!-- Recharge / Adjust Balance Modal -->
|
||||
<AModal v-model:visible="showRechargeModal" title="充值 / 调整余额" :width="420" :on-before-ok="handleRecharge" :ok-loading="rechargeLoading">
|
||||
<div class="mb-3 text-sm" style="color: var(--ta-text-tertiary)">
|
||||
当前余额:${{ walletInfo ? parseFloat(walletInfo.balance).toFixed(2) : '0.00' }}
|
||||
<!-- Adjust Balance Modal -->
|
||||
<AModal v-model:visible="showRechargeModal" title="调整余额" :width="440" :on-before-ok="handleRecharge" :ok-loading="rechargeLoading">
|
||||
<div class="mb-3 text-sm leading-6" style="color: var(--ta-text-secondary)">
|
||||
手动调整该租户的美元钱包余额(正数增加、负数扣减),用于运营补偿、余额纠错等场景。
|
||||
<br />
|
||||
总余额 <span class="money">${{ walletInfo ? parseFloat(walletInfo.balance).toFixed(6) : '0.000000' }}</span>
|
||||
| 可用余额 <span class="money">${{ walletInfo ? (walletInfo.balance - walletInfo.frozen_balance).toFixed(6) : '0.000000' }}</span>
|
||||
<span style="color: var(--ta-text-tertiary)">(扣减上限)</span>
|
||||
</div>
|
||||
<AForm :model="rechargeForm" layout="vertical">
|
||||
<AFormItem label="金额" required>
|
||||
<AInputNumber v-model="rechargeForm.amount" :precision="2" :step="1" placeholder="正数=充值,负数=扣减" class="w-full">
|
||||
<AFormItem label="调整金额(USD)" required>
|
||||
<AInputNumber v-model="rechargeForm.amount" :precision="2" :step="1" placeholder="正数=增加余额,负数=扣减余额" class="w-full">
|
||||
<template #prefix>$</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="描述">
|
||||
<ATextarea v-model="rechargeForm.description" placeholder="操作原因(选填,默认自动生成)" :auto-size="{ minRows: 2, maxRows: 4 }" />
|
||||
<AFormItem label="调整说明">
|
||||
<ATextarea v-model="rechargeForm.description" placeholder="如:运营补偿 / 余额纠错。建议填写,便于对账与审计" :auto-size="{ minRows: 2, maxRows: 4 }" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<div class="mt-2 text-xs" style="color: var(--ta-text-tertiary)">
|
||||
提示:输入正数充值余额,输入负数扣减余额
|
||||
提示:扣减金额不能超过可用余额(不含冻结部分);如需人民币线下转账到账,请使用「线下入账」
|
||||
</div>
|
||||
</AModal>
|
||||
|
||||
<!-- Offline Recharge Modal -->
|
||||
<AModal v-model:visible="showOfflineRechargeModal" title="线下充值入账" :width="440" :on-before-ok="handleOfflineRecharge" :ok-loading="offlineRechargeLoading">
|
||||
<div class="mb-3 text-sm" style="color: var(--ta-text-tertiary)">
|
||||
用于线下银行转账入账:输入人民币金额,到账按平台汇率换算为美元,并累计到累计充值(影响租户等级)。
|
||||
</div>
|
||||
<AForm :model="offlineRechargeForm" layout="vertical">
|
||||
<AFormItem label="入账金额(人民币)" required>
|
||||
<AInputNumber v-model="offlineRechargeForm.amount" :min="0.01" :precision="2" placeholder="如 100" class="w-full">
|
||||
<template #prefix>¥</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="转账流水号">
|
||||
<AInput v-model="offlineRechargeForm.transaction_no" placeholder="银行转账流水号(选填,用于对账与防重复入账)" />
|
||||
</AFormItem>
|
||||
<AFormItem label="入账说明" required>
|
||||
<ATextarea v-model="offlineRechargeForm.description" placeholder="如:客户名称 / 转账原因" :auto-size="{ minRows: 2, maxRows: 4 }" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<div class="mt-2 text-xs" style="color: var(--ta-text-tertiary)">
|
||||
到账金额 = 人民币金额 × 平台汇率,精确到 6 位小数
|
||||
</div>
|
||||
</AModal>
|
||||
|
||||
|
||||
Reference in new issue
Block a user