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'