diff --git a/frontend/src/components/payment/PaymentMethodSelector.vue b/frontend/src/components/payment/PaymentMethodSelector.vue
index a88f91f2e5..2c02340ed4 100644
--- a/frontend/src/components/payment/PaymentMethodSelector.vue
+++ b/frontend/src/components/payment/PaymentMethodSelector.vue
@@ -39,7 +39,7 @@
diff --git a/frontend/src/components/payment/PaymentQRDialog.vue b/frontend/src/components/payment/PaymentQRDialog.vue
index f6278e93e0..7dff831a6a 100644
--- a/frontend/src/components/payment/PaymentQRDialog.vue
+++ b/frontend/src/components/payment/PaymentQRDialog.vue
@@ -79,7 +79,7 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
-import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
+import { getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import type { PaymentOrder } from '@/types/payment'
import { currencySymbol } from '@/components/payment/currency'
import QRCode from 'qrcode'
@@ -122,8 +122,8 @@ let lastVerifyAt = 0
const VERIFY_RETRY_INTERVAL_MS = 15000
const VERIFY_RETRY_MAX_ATTEMPTS = 6
-const isAlipay = computed(() => props.paymentType.includes('alipay'))
-const isWxpay = computed(() => props.paymentType.includes('wxpay'))
+const isAlipay = computed(() => isBuiltInAlipayMethod(props.paymentType))
+const isWxpay = computed(() => isBuiltInWxpayMethod(props.paymentType))
const dialogTitle = computed(() => {
if (success.value) return t('payment.result.success')
diff --git a/frontend/src/components/payment/PaymentStatusPanel.vue b/frontend/src/components/payment/PaymentStatusPanel.vue
index d77db58a2f..c7232fd640 100644
--- a/frontend/src/components/payment/PaymentStatusPanel.vue
+++ b/frontend/src/components/payment/PaymentStatusPanel.vue
@@ -79,7 +79,7 @@
-
+
@@ -128,13 +128,14 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
-import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
+import { getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import { currencySymbol, formatPaymentAmount, normalizePaymentCurrency } from '@/components/payment/currency'
import type { PaymentOrder } from '@/types/payment'
import Icon from '@/components/icons/Icon.vue'
import QRCode from 'qrcode'
import alipayIcon from '@/assets/icons/alipay.svg'
import wxpayIcon from '@/assets/icons/wxpay.svg'
+import paymentIcon from '@/assets/icons/payment.svg'
const props = defineProps<{
orderId: number
@@ -182,8 +183,8 @@ let lastVerifyAt = 0
const VERIFY_RETRY_INTERVAL_MS = 15000
const VERIFY_RETRY_MAX_ATTEMPTS = 6
-const isAlipay = computed(() => props.paymentType.includes('alipay'))
-const isWxpay = computed(() => props.paymentType.includes('wxpay'))
+const isAlipay = computed(() => isBuiltInAlipayMethod(props.paymentType))
+const isWxpay = computed(() => isBuiltInWxpayMethod(props.paymentType))
const qrBorderClass = computed(() => {
if (isAlipay.value) return 'border-[#00AEEF] bg-blue-50 dark:border-[#00AEEF]/70 dark:bg-blue-950/20'
@@ -197,6 +198,12 @@ const qrLogoBgClass = computed(() => {
return 'bg-gray-400'
})
+const qrLogoIcon = computed(() => {
+ if (isAlipay.value) return alipayIcon
+ if (isWxpay.value) return wxpayIcon
+ return paymentIcon
+})
+
const scanTitle = computed(() => {
if (isAlipay.value) return t('payment.qr.scanAlipay')
if (isWxpay.value) return t('payment.qr.scanWxpay')
diff --git a/frontend/src/components/payment/__tests__/PaymentStatusPanel.spec.ts b/frontend/src/components/payment/__tests__/PaymentStatusPanel.spec.ts
index 7e39247831..d5919867c5 100644
--- a/frontend/src/components/payment/__tests__/PaymentStatusPanel.spec.ts
+++ b/frontend/src/components/payment/__tests__/PaymentStatusPanel.spec.ts
@@ -132,6 +132,28 @@ describe('PaymentStatusPanel', () => {
openSpy.mockRestore()
})
+ it('uses generic QR copy for custom methods that contain built-in names', async () => {
+ const wrapper = mount(PaymentStatusPanel, {
+ props: {
+ orderId: 42,
+ qrCode: 'https://pay.example.com/qr/42',
+ expiresAt: '2099-01-01T12:30:00Z',
+ paymentType: 'card_alipay',
+ orderType: 'balance',
+ },
+ global: {
+ stubs: {
+ Icon: true,
+ },
+ },
+ })
+
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('payment.qr.scanToPay')
+ expect(wrapper.text()).not.toContain('payment.qr.scanAlipay')
+ })
+
it('actively verifies a stuck pending order and settles it when upstream confirms payment', async () => {
pollOrderStatus.mockResolvedValue(orderFactory('PENDING'))
verifyOrder.mockResolvedValue({
diff --git a/frontend/src/components/payment/__tests__/providerConfig.spec.ts b/frontend/src/components/payment/__tests__/providerConfig.spec.ts
index 4c20ae725c..267693b5cb 100644
--- a/frontend/src/components/payment/__tests__/providerConfig.spec.ts
+++ b/frontend/src/components/payment/__tests__/providerConfig.spec.ts
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'
import {
PAYMENT_CURRENCY_OPTIONS,
PROVIDER_CONFIG_FIELDS,
+ isBuiltInAlipayMethod,
+ isBuiltInWxpayMethod,
parseEasyPayCustomMethods,
serializeEasyPayCustomMethods,
} from '@/components/payment/providerConfig'
@@ -79,3 +81,15 @@ describe('EasyPay custom methods config', () => {
expect(serializeEasyPayCustomMethods([{ type: '', upstreamType: 'epay', displayName: 'LDC' }])).toBe('')
})
})
+
+describe('built-in payment method helpers', () => {
+ it('only treats exact built-in aliases as Alipay or WeChat Pay', () => {
+ expect(isBuiltInAlipayMethod('alipay')).toBe(true)
+ expect(isBuiltInAlipayMethod('alipay_direct')).toBe(true)
+ expect(isBuiltInAlipayMethod('card_alipay')).toBe(false)
+
+ expect(isBuiltInWxpayMethod('wxpay')).toBe(true)
+ expect(isBuiltInWxpayMethod('wxpay_direct')).toBe(true)
+ expect(isBuiltInWxpayMethod('card_wxpay')).toBe(false)
+ })
+})
diff --git a/frontend/src/components/payment/providerConfig.ts b/frontend/src/components/payment/providerConfig.ts
index 203bfb6818..395c32725f 100644
--- a/frontend/src/components/payment/providerConfig.ts
+++ b/frontend/src/components/payment/providerConfig.ts
@@ -50,6 +50,14 @@ export const EASYPAY_PAYMENT_MODES = ['qrcode', 'popup'] as const
/** Fixed display order for user-facing payment methods */
export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct', 'stripe', 'airwallex'] as const
+export function isBuiltInAlipayMethod(type: string): boolean {
+ return type === 'alipay' || type === 'alipay_direct'
+}
+
+export function isBuiltInWxpayMethod(type: string): boolean {
+ return type === 'wxpay' || type === 'wxpay_direct'
+}
+
/** Payment mode constants */
export const PAYMENT_MODE_QRCODE = 'qrcode'
export const PAYMENT_MODE_POPUP = 'popup'
diff --git a/frontend/src/views/user/PaymentQRCodeView.vue b/frontend/src/views/user/PaymentQRCodeView.vue
index f844858daf..5df67d0fe9 100644
--- a/frontend/src/views/user/PaymentQRCodeView.vue
+++ b/frontend/src/views/user/PaymentQRCodeView.vue
@@ -41,6 +41,7 @@ import { usePaymentStore } from '@/stores/payment'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
import { useAppStore } from '@/stores'
+import { isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import QRCode from 'qrcode'
import alipayIcon from '@/assets/icons/alipay.svg'
import wxpayIcon from '@/assets/icons/wxpay.svg'
@@ -69,8 +70,8 @@ const countdownDisplay = computed(() => {
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
})
-const isAlipay = computed(() => paymentType.value.includes('alipay'))
-const isWxpay = computed(() => paymentType.value.includes('wxpay'))
+const isAlipay = computed(() => isBuiltInAlipayMethod(paymentType.value))
+const isWxpay = computed(() => isBuiltInWxpayMethod(paymentType.value))
const scanTitle = computed(() => {
if (isAlipay.value) return t('payment.qr.scanAlipay')
diff --git a/frontend/src/views/user/PaymentView.vue b/frontend/src/views/user/PaymentView.vue
index 6e29061013..6d1d2fd31e 100644
--- a/frontend/src/views/user/PaymentView.vue
+++ b/frontend/src/views/user/PaymentView.vue
@@ -267,7 +267,7 @@ import type { SubscriptionPlan, CheckoutInfoResponse, CreateOrderResult, OrderTy
import AppLayout from '@/components/layout/AppLayout.vue'
import AmountInput from '@/components/payment/AmountInput.vue'
import PaymentMethodSelector from '@/components/payment/PaymentMethodSelector.vue'
-import { METHOD_ORDER, getPaymentPopupFeatures } from '@/components/payment/providerConfig'
+import { METHOD_ORDER, getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import {
PAYMENT_RECOVERY_STORAGE_KEY,
buildCreateOrderPayload,
@@ -697,8 +697,8 @@ watch(() => [validAmount.value, selectedMethod.value] as const, ([amt, method])
const paymentButtonClass = computed(() => {
const m = selectedMethod.value
if (!m) return 'btn-primary'
- if (m.includes('alipay')) return 'btn-alipay'
- if (m.includes('wxpay')) return 'btn-wxpay'
+ if (isBuiltInAlipayMethod(m)) return 'btn-alipay'
+ if (isBuiltInWxpayMethod(m)) return 'btn-wxpay'
if (m === 'stripe') return 'btn-stripe'
if (m === 'airwallex') return 'btn-airwallex'
return 'btn-primary'