fix(payment): integrate recharge fee rate in order flow and fix UI display

Backend:
- Use cfg.RechargeFeeRate in order creation instead of hardcoded 0
- Remove dead getFeeRate stub method
- All amounts computed server-side: order_amount, pay_amount, fee_rate

Frontend - PaymentView:
- Read recharge_fee_rate from checkout-info API (not per-method)
- Show fee breakdown only when fee_rate > 0
- Show credited amount only when multiplier ≠ 1

Frontend - Order display (user + admin):
- Fix fee_rate * 100 bug (fee_rate is already a percentage)
- OrderTable: show pay_amount as primary, fee/credited as sub-lines
- AdminOrderDetail: full breakdown (base/fee/paid/credited)
- AdminRefundDialog: label "到账金额" for clarity
- PaymentResultView: show pay_amount with fee info

Types + i18n:
- Add recharge_fee_rate to CheckoutInfoResponse
- Add fee_rate to CreateOrderResult
- Add translations: creditedAmount, fee, baseAmount, includedInPayAmount
This commit is contained in:
erio
2026-04-15 01:04:01 +08:00
parent 98140f6cac
commit e761d38fd1
12 changed files with 71 additions and 32 deletions

View File

@@ -18,23 +18,27 @@
</span>
</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.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.baseAmount') }}</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ baseAmount.toFixed(2) }}</p>
</div>
<div v-if="order.fee_rate > 0">
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.fee') }} ({{ order.fee_rate }}%)</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ feeAmount.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>
</div>
<div v-if="order.amount !== order.pay_amount">
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</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.paymentMethod') }}</p>
<p class="text-sm text-gray-700 dark:text-gray-300">
{{ t('payment.methods.' + order.payment_type, order.payment_type) }}
</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.feeRate') }}</p>
<p class="text-sm text-gray-700 dark:text-gray-300">{{ (order.fee_rate * 100).toFixed(1) }}%</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.orderType') }}</p>
<p class="text-sm text-gray-700 dark:text-gray-300">
@@ -110,6 +114,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue'
import type { PaymentOrder } from '@/types/payment'
@@ -117,11 +122,24 @@ import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } f
const { t } = useI18n()
defineProps<{
const props = defineProps<{
show: boolean
order: PaymentOrder | null
}>()
/** 充值金额 (base amount before fee) = pay_amount - fee = pay_amount / (1 + fee_rate/100) */
const baseAmount = computed(() => {
if (!props.order) return 0
if (props.order.fee_rate <= 0) return props.order.pay_amount
return props.order.pay_amount / (1 + props.order.fee_rate / 100)
})
/** 手续费 = pay_amount - baseAmount */
const feeAmount = computed(() => {
if (!props.order || props.order.fee_rate <= 0) return 0
return props.order.pay_amount - baseAmount.value
})
const emit = defineEmits<{
(e: 'close'): void
(e: 'cancel', order: PaymentOrder): void

View File

@@ -51,12 +51,15 @@
<span class="text-sm text-gray-600 dark:text-gray-400">#{{ value }}</span>
</template>
<template #cell-amount="{ value, row }">
<template #cell-pay_amount="{ value, row }">
<div class="text-sm">
<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) }})
<span class="font-medium text-gray-900 dark:text-white">¥{{ value.toFixed(2) }}</span>
<span v-if="row.fee_rate > 0" class="ml-1 text-xs text-gray-400" :title="t('payment.orders.fee') + ': ' + row.fee_rate + '%'">
({{ row.fee_rate }}%)
</span>
<div v-if="row.amount !== row.pay_amount" class="text-xs text-gray-500">
{{ t('payment.orders.creditedAmount') }}: {{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.amount.toFixed(2) }}
</div>
</div>
</template>
@@ -183,7 +186,7 @@ function emitFiltersChanged() {
const columns = computed<Column[]>(() => [
{ key: 'id', label: t('payment.orders.orderId') },
{ key: 'user_id', label: t('payment.orders.userId') },
{ key: 'amount', label: t('payment.orders.amount') },
{ key: 'pay_amount', label: t('payment.orders.payAmount') },
{ key: 'payment_type', label: t('payment.orders.paymentMethod') },
{ key: 'status', label: t('payment.orders.status') },
{ key: 'order_type', label: t('payment.orders.orderType') },

View File

@@ -34,7 +34,7 @@
<span class="font-mono text-gray-900 dark:text-white">#{{ order?.id }}</span>
</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="text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</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">

View File

@@ -12,10 +12,15 @@
<span v-if="row.user_notes" class="ml-1 text-xs text-gray-400">({{ row.user_notes }})</span>
</div>
</template>
<template #cell-amount="{ value, row }">
<template #cell-pay_amount="{ value, row }">
<div class="text-sm">
<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">(¥{{ row.pay_amount.toFixed(2) }})</span>
<span class="font-medium text-gray-900 dark:text-white">¥{{ value.toFixed(2) }}</span>
<span v-if="row.fee_rate > 0" class="ml-1 text-xs text-gray-400" :title="t('payment.orders.fee') + ': ' + row.fee_rate + '%'">
({{ t('payment.orders.fee') }} {{ row.fee_rate }}%)
</span>
<div v-if="row.amount !== row.pay_amount" class="text-xs text-gray-500">
{{ t('payment.orders.creditedAmount') }}: {{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.amount.toFixed(2) }}
</div>
</div>
</template>
<template #cell-payment_type="{ value }">
@@ -60,7 +65,7 @@ const columns = computed((): Column[] => {
cols.push({ key: 'user_email', label: t('payment.admin.colUser') })
}
cols.push(
{ key: 'amount', label: t('payment.orders.amount') },
{ key: 'pay_amount', label: t('payment.orders.payAmount') },
{ key: 'payment_type', label: t('payment.orders.paymentMethod') },
{ key: 'status', label: t('payment.orders.status') },
{ key: 'created_at', label: t('payment.orders.createdAt') },