feat(payment): balance recharge multiplier and refund amount separation

- Add balance_recharge_multiplier system setting (e.g. 1.2 = charge 100 get 120)
- Separate order_amount (credited balance) from pay_amount (actual payment)
- Refund calculates gateway amount proportionally from pay_amount
- Frontend shows both amounts in order details, payment status, refund dialog
- Admin settings UI for configuring recharge multiplier
This commit is contained in:
erio
2026-04-15 00:14:57 +08:00
parent 7c671b5373
commit 60a4b9316b
24 changed files with 246 additions and 101 deletions

View File

@@ -19,11 +19,11 @@
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">${{ order.amount.toFixed(2) }}</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">${{ order.pay_amount.toFixed(2) }}</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ order.pay_amount.toFixed(2) }}</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p>
@@ -73,7 +73,7 @@
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundAmount') }}:</span>
<span class="ml-1 font-medium text-red-700 dark:text-red-300">${{ order.refund_amount.toFixed(2) }}</span>
<span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.refund_amount.toFixed(2) }}</span>
</div>
<div v-if="order.refund_reason" class="col-span-2">
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundReason') }}:</span>

View File

@@ -53,9 +53,9 @@
<template #cell-amount="{ value, row }">
<div class="text-sm">
<span class="font-medium text-gray-900 dark:text-white">${{ value.toFixed(2) }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{ row.order_type === 'balance' ? '$' : '¥' }}{{ value.toFixed(2) }}</span>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">
({{ t('payment.orders.payAmount') }}: ${{ row.pay_amount.toFixed(2) }})
({{ t('payment.orders.payAmount') }}: ¥{{ row.pay_amount.toFixed(2) }})
</span>
</div>
</template>

View File

@@ -35,11 +35,15 @@
</div>
<div class="mt-1 flex justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span>
<span class="font-medium text-gray-900 dark:text-white">${{ order?.pay_amount?.toFixed(2) }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</span>
</div>
<div class="mt-1 flex justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
<span class="font-medium text-gray-900 dark:text-white">¥{{ order?.pay_amount?.toFixed(2) }}</span>
</div>
<div v-if="actuallyRefunded > 0" class="mt-1 flex justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.alreadyRefunded') }}</span>
<span class="font-medium text-red-600 dark:text-red-400">${{ actuallyRefunded.toFixed(2) }}</span>
<span class="font-medium text-red-600 dark:text-red-400">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ actuallyRefunded.toFixed(2) }}</span>
</div>
</div>
@@ -66,7 +70,7 @@
</div>
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700">
<div class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.orderAmount') }}</div>
<div class="mt-1 font-semibold text-gray-900 dark:text-white">${{ order?.pay_amount?.toFixed(2) }}</div>
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</div>
</div>
</div>
@@ -91,7 +95,7 @@
<div>
<label class="input-label">{{ t('payment.admin.refundAmount') }}</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">$</span>
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ order?.order_type === 'balance' ? '$' : '¥' }}</span>
<input
v-model.number="form.amount"
type="number"
@@ -103,7 +107,7 @@
/>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('payment.admin.maxRefundable') }}: ${{ maxRefundable.toFixed(2) }}
{{ t('payment.admin.maxRefundable') }}: {{ order?.order_type === 'balance' ? '$' : '¥' }}{{ maxRefundable.toFixed(2) }}
</p>
</div>
@@ -200,12 +204,12 @@ const actuallyRefunded = computed(() => {
const maxRefundable = computed(() => {
if (!props.order) return 0
return props.order.pay_amount - actuallyRefunded.value
return props.order.amount - actuallyRefunded.value
})
const balanceInsufficient = computed(() => {
if (props.userBalance == null || !props.order) return false
return props.userBalance < props.order.pay_amount
return props.userBalance < props.order.amount
})
watch(() => props.show, (val) => {