feat(payment): 支持强制移动端统一使用二维码支付

This commit is contained in:
wucm667 2026-05-19 18:22:12 +08:00
parent 8927ab091e
commit e4c7927eff
13 changed files with 144 additions and 3 deletions

View File

@ -278,6 +278,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow, PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow,
PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit, PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode, PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
PaymentAlipayForceQRCode: paymentCfg.AlipayForceQRCode,
ChannelMonitorEnabled: settings.ChannelMonitorEnabled, ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds, ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
@ -603,6 +604,9 @@ type UpdateSettingsRequest struct {
PaymentCancelRateLimitUnit *string `json:"payment_cancel_rate_limit_unit"` PaymentCancelRateLimitUnit *string `json:"payment_cancel_rate_limit_unit"`
PaymentCancelRateLimitMode *string `json:"payment_cancel_rate_limit_window_mode"` PaymentCancelRateLimitMode *string `json:"payment_cancel_rate_limit_window_mode"`
// Force Alipay mobile clients to use QR code payment instead of mobile redirect
PaymentAlipayForceQRCode *bool `json:"payment_alipay_force_qrcode"`
// Channel Monitor feature switch // Channel Monitor feature switch
ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"` ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"` ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
@ -1774,6 +1778,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow, CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit, CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
CancelRateLimitMode: req.PaymentCancelRateLimitMode, CancelRateLimitMode: req.PaymentCancelRateLimitMode,
AlipayForceQRCode: req.PaymentAlipayForceQRCode,
} }
if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil { if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
@ -1981,6 +1986,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow, PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit, PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode, PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled, ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds, ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
@ -2022,7 +2028,8 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil || req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil ||
req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil || req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
req.PaymentCancelRateLimitMax != nil || req.PaymentCancelRateLimitWindow != nil || req.PaymentCancelRateLimitMax != nil || req.PaymentCancelRateLimitWindow != nil ||
req.PaymentCancelRateLimitUnit != nil || req.PaymentCancelRateLimitMode != nil req.PaymentCancelRateLimitUnit != nil || req.PaymentCancelRateLimitMode != nil ||
req.PaymentAlipayForceQRCode != nil
} }
func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.SystemSettings, after *service.SystemSettings, beforeAuthSourceDefaults *service.AuthSourceDefaultSettings, afterAuthSourceDefaults *service.AuthSourceDefaultSettings, req UpdateSettingsRequest) { func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.SystemSettings, after *service.SystemSettings, beforeAuthSourceDefaults *service.AuthSourceDefaultSettings, afterAuthSourceDefaults *service.AuthSourceDefaultSettings, req UpdateSettingsRequest) {

View File

@ -218,6 +218,9 @@ type SystemSettings struct {
PaymentCancelRateLimitUnit string `json:"payment_cancel_rate_limit_unit"` PaymentCancelRateLimitUnit string `json:"payment_cancel_rate_limit_unit"`
PaymentCancelRateLimitMode string `json:"payment_cancel_rate_limit_window_mode"` PaymentCancelRateLimitMode string `json:"payment_cancel_rate_limit_window_mode"`
// Force Alipay mobile clients to use QR code payment instead of mobile redirect
PaymentAlipayForceQRCode bool `json:"payment_alipay_force_qrcode"`
// Balance low notification // Balance low notification
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"` BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"` BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`

View File

@ -141,6 +141,7 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
HelpText: cfg.HelpText, HelpText: cfg.HelpText,
HelpImageURL: cfg.HelpImageURL, HelpImageURL: cfg.HelpImageURL,
StripePublishableKey: cfg.StripePublishableKey, StripePublishableKey: cfg.StripePublishableKey,
AlipayForceQRCode: cfg.AlipayForceQRCode,
}) })
} }
@ -155,6 +156,7 @@ type checkoutInfoResponse struct {
HelpText string `json:"help_text"` HelpText string `json:"help_text"`
HelpImageURL string `json:"help_image_url"` HelpImageURL string `json:"help_image_url"`
StripePublishableKey string `json:"stripe_publishable_key"` StripePublishableKey string `json:"stripe_publishable_key"`
AlipayForceQRCode bool `json:"alipay_force_qrcode"`
} }
type checkoutPlan struct { type checkoutPlan struct {

View File

@ -858,6 +858,7 @@ func TestAPIContracts(t *testing.T) {
"payment_cancel_rate_limit_window": 0, "payment_cancel_rate_limit_window": 0,
"payment_cancel_rate_limit_unit": "", "payment_cancel_rate_limit_unit": "",
"payment_cancel_rate_limit_window_mode": "", "payment_cancel_rate_limit_window_mode": "",
"payment_alipay_force_qrcode": false,
"balance_low_notify_enabled": false, "balance_low_notify_enabled": false,
"account_quota_notify_enabled": false, "account_quota_notify_enabled": false,
"balance_low_notify_threshold": 0, "balance_low_notify_threshold": 0,
@ -1080,6 +1081,7 @@ func TestAPIContracts(t *testing.T) {
"payment_cancel_rate_limit_window": 0, "payment_cancel_rate_limit_window": 0,
"payment_cancel_rate_limit_unit": "", "payment_cancel_rate_limit_unit": "",
"payment_cancel_rate_limit_window_mode": "", "payment_cancel_rate_limit_window_mode": "",
"payment_alipay_force_qrcode": false,
"balance_low_notify_enabled": false, "balance_low_notify_enabled": false,
"account_quota_notify_enabled": false, "account_quota_notify_enabled": false,
"balance_low_notify_threshold": 0, "balance_low_notify_threshold": 0,

View File

@ -34,6 +34,7 @@ const (
SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW" SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW"
SettingCancelWindowUnit = "CANCEL_RATE_LIMIT_UNIT" SettingCancelWindowUnit = "CANCEL_RATE_LIMIT_UNIT"
SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE" SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE"
SettingAlipayForceQRCode = "ALIPAY_FORCE_QRCODE"
) )
// Default values for payment configuration settings. // Default values for payment configuration settings.
@ -67,6 +68,9 @@ type PaymentConfig struct {
CancelRateLimitWindow int `json:"cancel_rate_limit_window"` CancelRateLimitWindow int `json:"cancel_rate_limit_window"`
CancelRateLimitUnit string `json:"cancel_rate_limit_unit"` CancelRateLimitUnit string `json:"cancel_rate_limit_unit"`
CancelRateLimitMode string `json:"cancel_rate_limit_window_mode"` CancelRateLimitMode string `json:"cancel_rate_limit_window_mode"`
// Force Alipay mobile users to use QR code instead of mobile redirect
AlipayForceQRCode bool `json:"alipay_force_qrcode"`
} }
// UpdatePaymentConfigRequest contains fields to update payment configuration. // UpdatePaymentConfigRequest contains fields to update payment configuration.
@ -94,6 +98,9 @@ type UpdatePaymentConfigRequest struct {
CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"` CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"` CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
// Force Alipay mobile users to use QR code instead of mobile redirect
AlipayForceQRCode *bool `json:"alipay_force_qrcode"`
VisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"` VisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
VisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"` VisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
VisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"` VisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
@ -202,6 +209,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
SettingHelpImageURL, SettingHelpText, SettingHelpImageURL, SettingHelpText,
SettingCancelRateLimitOn, SettingCancelRateLimitMax, SettingCancelRateLimitOn, SettingCancelRateLimitMax,
SettingCancelWindowSize, SettingCancelWindowUnit, SettingCancelWindowMode, SettingCancelWindowSize, SettingCancelWindowUnit, SettingCancelWindowMode,
SettingAlipayForceQRCode,
SettingPaymentVisibleMethodAlipayEnabled, SettingPaymentVisibleMethodAlipaySource, SettingPaymentVisibleMethodAlipayEnabled, SettingPaymentVisibleMethodAlipaySource,
SettingPaymentVisibleMethodWxpayEnabled, SettingPaymentVisibleMethodWxpaySource, SettingPaymentVisibleMethodWxpayEnabled, SettingPaymentVisibleMethodWxpaySource,
} }
@ -237,6 +245,8 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
CancelRateLimitWindow: pcParseInt(vals[SettingCancelWindowSize], 1), CancelRateLimitWindow: pcParseInt(vals[SettingCancelWindowSize], 1),
CancelRateLimitUnit: vals[SettingCancelWindowUnit], CancelRateLimitUnit: vals[SettingCancelWindowUnit],
CancelRateLimitMode: vals[SettingCancelWindowMode], CancelRateLimitMode: vals[SettingCancelWindowMode],
AlipayForceQRCode: vals[SettingAlipayForceQRCode] == "true",
} }
if cfg.LoadBalanceStrategy == "" { if cfg.LoadBalanceStrategy == "" {
cfg.LoadBalanceStrategy = payment.DefaultLoadBalanceStrategy cfg.LoadBalanceStrategy = payment.DefaultLoadBalanceStrategy
@ -314,6 +324,7 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow), SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow),
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit), SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit),
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode), SettingCancelWindowMode: derefStr(req.CancelRateLimitMode),
SettingAlipayForceQRCode: formatBoolOrEmpty(req.AlipayForceQRCode),
SettingPaymentVisibleMethodAlipaySource: derefStr(req.VisibleMethodAlipaySource), SettingPaymentVisibleMethodAlipaySource: derefStr(req.VisibleMethodAlipaySource),
SettingPaymentVisibleMethodWxpaySource: derefStr(req.VisibleMethodWxpaySource), SettingPaymentVisibleMethodWxpaySource: derefStr(req.VisibleMethodWxpaySource),
SettingPaymentVisibleMethodAlipayEnabled: formatBoolOrEmpty(req.VisibleMethodAlipayEnabled), SettingPaymentVisibleMethodAlipayEnabled: formatBoolOrEmpty(req.VisibleMethodAlipayEnabled),

View File

@ -528,6 +528,7 @@ export interface SystemSettings {
payment_cancel_rate_limit_window: number; payment_cancel_rate_limit_window: number;
payment_cancel_rate_limit_unit: string; payment_cancel_rate_limit_unit: string;
payment_cancel_rate_limit_window_mode: string; payment_cancel_rate_limit_window_mode: string;
payment_alipay_force_qrcode?: boolean;
payment_visible_method_alipay_source?: string; payment_visible_method_alipay_source?: string;
payment_visible_method_wxpay_source?: string; payment_visible_method_wxpay_source?: string;
payment_visible_method_alipay_enabled?: boolean; payment_visible_method_alipay_enabled?: boolean;
@ -745,6 +746,7 @@ export interface UpdateSettingsRequest {
payment_cancel_rate_limit_window?: number; payment_cancel_rate_limit_window?: number;
payment_cancel_rate_limit_unit?: string; payment_cancel_rate_limit_unit?: string;
payment_cancel_rate_limit_window_mode?: string; payment_cancel_rate_limit_window_mode?: string;
payment_alipay_force_qrcode?: boolean;
payment_visible_method_alipay_source?: string; payment_visible_method_alipay_source?: string;
payment_visible_method_wxpay_source?: string; payment_visible_method_wxpay_source?: string;
payment_visible_method_alipay_enabled?: boolean; payment_visible_method_alipay_enabled?: boolean;

View File

@ -220,6 +220,36 @@ describe('decidePaymentLaunch', () => {
expect(decision.jsapi?.appId).toBe('wx123') expect(decision.jsapi?.appId).toBe('wx123')
expect(decision.paymentState.orderType).toBe('subscription') expect(decision.paymentState.orderType).toBe('subscription')
}) })
it('forces qr_waiting for mobile alipay when forceQRCode is enabled', () => {
const decision = decidePaymentLaunch(createOrderResult({
pay_url: 'https://pay.example.com/mobile/session',
qr_code: 'https://pay.example.com/qr/session',
}), {
visibleMethod: 'alipay',
orderType: 'balance',
isMobile: true,
forceQRCode: true,
})
expect(decision.kind).toBe('qr_waiting')
expect(decision.paymentState.qrCode).toBe('https://pay.example.com/qr/session')
})
it('does not affect non-alipay methods when forceQRCode is enabled', () => {
const decision = decidePaymentLaunch(createOrderResult({
pay_url: 'https://pay.example.com/mobile/session',
qr_code: 'https://pay.example.com/qr/session',
}), {
visibleMethod: 'wxpay',
orderType: 'balance',
isMobile: true,
forceQRCode: true,
})
// wxpay mobile with pay_url still redirects
expect(decision.kind).toBe('redirect_waiting')
})
}) })
describe('buildCreateOrderPayload', () => { describe('buildCreateOrderPayload', () => {
@ -260,6 +290,34 @@ describe('buildCreateOrderPayload', () => {
payment_source: 'wechat_in_app_resume', payment_source: 'wechat_in_app_resume',
}) })
}) })
it('passes is_mobile: false when forceQRCode is enabled for alipay', () => {
expect(buildCreateOrderPayload({
amount: 50,
paymentType: 'alipay',
orderType: 'balance',
origin: 'https://app.example.com',
isMobile: true,
isWechatBrowser: false,
forceQRCode: true,
})).toMatchObject({
is_mobile: false,
})
})
it('still passes is_mobile: true when forceQRCode is enabled for non-alipay methods', () => {
expect(buildCreateOrderPayload({
amount: 50,
paymentType: 'wxpay',
orderType: 'balance',
origin: 'https://app.example.com',
isMobile: true,
isWechatBrowser: false,
forceQRCode: true,
})).toMatchObject({
is_mobile: true,
})
})
}) })
describe('readPaymentRecoverySnapshot', () => { describe('readPaymentRecoverySnapshot', () => {

View File

@ -55,6 +55,8 @@ export interface PaymentLaunchContext {
orderType: OrderType orderType: OrderType
isMobile: boolean isMobile: boolean
isWechatBrowser?: boolean isWechatBrowser?: boolean
/** When true, Alipay payments always use QR code regardless of device type */
forceQRCode?: boolean
now?: number now?: number
stripePopupUrl?: string stripePopupUrl?: string
stripeRouteUrl?: string stripeRouteUrl?: string
@ -78,6 +80,8 @@ export interface BuildCreateOrderPayloadInput {
origin?: string origin?: string
isMobile: boolean isMobile: boolean
isWechatBrowser: boolean isWechatBrowser: boolean
/** When true, Alipay payments always use QR code (passes is_mobile: false to backend) */
forceQRCode?: boolean
} }
type CreateOrderFlowResult = CreateOrderResult & { type CreateOrderFlowResult = CreateOrderResult & {
@ -111,11 +115,16 @@ export function getVisibleMethods(methods: Record<string, MethodLimit>): Record<
export function buildCreateOrderPayload(input: BuildCreateOrderPayloadInput): CreateOrderRequest { export function buildCreateOrderPayload(input: BuildCreateOrderPayloadInput): CreateOrderRequest {
const visibleMethod = normalizeVisibleMethod(input.paymentType) || input.paymentType.trim() const visibleMethod = normalizeVisibleMethod(input.paymentType) || input.paymentType.trim()
const normalizedOrigin = (input.origin || '').trim().replace(/\/+$/, '') const normalizedOrigin = (input.origin || '').trim().replace(/\/+$/, '')
// When forceQRCode is enabled for alipay, always tell the backend this is not a mobile
// request so it generates a QR code instead of a mobile-redirect URL.
const effectiveMobile = (input.forceQRCode && visibleMethod === 'alipay')
? false
: input.isMobile
const payload: CreateOrderRequest = { const payload: CreateOrderRequest = {
amount: input.amount, amount: input.amount,
payment_type: visibleMethod, payment_type: visibleMethod,
order_type: input.orderType, order_type: input.orderType,
is_mobile: input.isMobile, is_mobile: effectiveMobile,
payment_source: visibleMethod === 'wxpay' && input.isWechatBrowser payment_source: visibleMethod === 'wxpay' && input.isWechatBrowser
? 'wechat_in_app_resume' ? 'wechat_in_app_resume'
: 'hosted_redirect', : 'hosted_redirect',
@ -190,9 +199,14 @@ export function decidePaymentLaunch(
} }
const normalizedPaymentMode = baseState.paymentMode.trim().toLowerCase() const normalizedPaymentMode = baseState.paymentMode.trim().toLowerCase()
// When forceQRCode is on for alipay, treat the device as desktop so the mobile-redirect
// branch is bypassed and we fall through to qr_waiting.
const effectiveMobile = (context.forceQRCode && visibleMethod === 'alipay')
? false
: context.isMobile
const prefersRedirect = normalizedPaymentMode === 'redirect' const prefersRedirect = normalizedPaymentMode === 'redirect'
|| normalizedPaymentMode === 'popup' || normalizedPaymentMode === 'popup'
|| (context.isMobile && !!baseState.payUrl) || (effectiveMobile && !!baseState.payUrl)
const prefersQr = normalizedPaymentMode === 'qrcode' const prefersQr = normalizedPaymentMode === 'qrcode'
|| normalizedPaymentMode === 'native' || normalizedPaymentMode === 'native'
|| (!prefersRedirect && !!baseState.qrCode) || (!prefersRedirect && !!baseState.qrCode)

View File

@ -5635,6 +5635,8 @@ export default {
cancelRateLimitWindowMode: 'Window Mode', cancelRateLimitWindowMode: 'Window Mode',
cancelRateLimitWindowModeRolling: 'Rolling', cancelRateLimitWindowModeRolling: 'Rolling',
cancelRateLimitWindowModeFixed: 'Fixed', cancelRateLimitWindowModeFixed: 'Fixed',
alipayForceQRCode: 'Force Alipay QR Code',
alipayForceQRCodeHint: 'When enabled, mobile Alipay users always see a QR code instead of being redirected to the mobile payment page',
helpText: 'Help Text', helpText: 'Help Text',
helpImageUrl: 'Help Image URL', helpImageUrl: 'Help Image URL',
manageProviders: 'Manage Providers', manageProviders: 'Manage Providers',

View File

@ -5795,6 +5795,8 @@ export default {
cancelRateLimitWindowMode: '窗口模式', cancelRateLimitWindowMode: '窗口模式',
cancelRateLimitWindowModeRolling: '滚动', cancelRateLimitWindowModeRolling: '滚动',
cancelRateLimitWindowModeFixed: '固定', cancelRateLimitWindowModeFixed: '固定',
alipayForceQRCode: '支付宝强制二维码支付',
alipayForceQRCodeHint: '启用后,移动端支付宝用户将统一使用二维码扫码支付,不再跳转至手机网站支付',
helpText: '帮助文本', helpText: '帮助文本',
helpImageUrl: '帮助图片链接', helpImageUrl: '帮助图片链接',
manageProviders: '管理服务商', manageProviders: '管理服务商',

View File

@ -69,6 +69,8 @@ export interface CheckoutInfoResponse {
help_text: string help_text: string
help_image_url: string help_image_url: string
stripe_publishable_key: string stripe_publishable_key: string
/** When true, Alipay payments on mobile always show the QR code instead of redirecting */
alipay_force_qrcode?: boolean
} }
// ==================== Orders ==================== // ==================== Orders ====================

View File

@ -5843,6 +5843,38 @@
> >
</div> </div>
</div> </div>
<div>
<label class="input-label">{{
t("admin.settings.payment.alipayForceQRCode")
}}</label>
<div class="flex items-center gap-2">
<button
type="button"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
form.payment_alipay_force_qrcode
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
@click="
form.payment_alipay_force_qrcode =
!form.payment_alipay_force_qrcode
"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
form.payment_alipay_force_qrcode
? 'translate-x-5'
: 'translate-x-0',
]"
/>
</button>
<span class="text-sm text-gray-500 dark:text-gray-400">{{
t("admin.settings.payment.alipayForceQRCodeHint")
}}</span>
</div>
</div>
</div> </div>
<!-- Row 4: Enabled payment types (provider badges like sub2apipay) --> <!-- Row 4: Enabled payment types (provider badges like sub2apipay) -->
<div> <div>
@ -6772,6 +6804,7 @@ const form = reactive<SettingsForm>({
payment_cancel_rate_limit_window: 1, payment_cancel_rate_limit_window: 1,
payment_cancel_rate_limit_unit: "day", payment_cancel_rate_limit_unit: "day",
payment_cancel_rate_limit_window_mode: "rolling", payment_cancel_rate_limit_window_mode: "rolling",
payment_alipay_force_qrcode: false,
table_default_page_size: tablePageSizeDefault, table_default_page_size: tablePageSizeDefault,
table_page_size_options: [10, 20, 50, 100], table_page_size_options: [10, 20, 50, 100],
custom_menu_items: [] as Array<{ custom_menu_items: [] as Array<{
@ -8036,6 +8069,7 @@ async function saveSettings() {
payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit, payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit,
payment_cancel_rate_limit_window_mode: payment_cancel_rate_limit_window_mode:
form.payment_cancel_rate_limit_window_mode, form.payment_cancel_rate_limit_window_mode,
payment_alipay_force_qrcode: form.payment_alipay_force_qrcode,
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled, openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
// Balance & quota notification // Balance & quota notification
balance_low_notify_enabled: form.balance_low_notify_enabled, balance_low_notify_enabled: form.balance_low_notify_enabled,

View File

@ -698,6 +698,7 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
origin: typeof window !== 'undefined' ? window.location.origin : '', origin: typeof window !== 'undefined' ? window.location.origin : '',
isMobile: isMobileDevice(), isMobile: isMobileDevice(),
isWechatBrowser: typeof window !== 'undefined' && /MicroMessenger/i.test(window.navigator.userAgent), isWechatBrowser: typeof window !== 'undefined' && /MicroMessenger/i.test(window.navigator.userAgent),
forceQRCode: !!(checkout.value.alipay_force_qrcode && normalizeVisibleMethod(requestType) === 'alipay'),
}) })
if (options.openid) { if (options.openid) {
payload.openid = options.openid payload.openid = options.openid
@ -745,6 +746,7 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
orderType, orderType,
isMobile: isMobileDevice(), isMobile: isMobileDevice(),
isWechatBrowser: typeof window !== 'undefined' && /MicroMessenger/i.test(window.navigator.userAgent), isWechatBrowser: typeof window !== 'undefined' && /MicroMessenger/i.test(window.navigator.userAgent),
forceQRCode: !!(checkout.value.alipay_force_qrcode && visibleMethod === 'alipay'),
stripePopupUrl: stripeRouteUrl, stripePopupUrl: stripeRouteUrl,
stripeRouteUrl, stripeRouteUrl,
airwallexRouteUrl, airwallexRouteUrl,