feat: 支付宝直连 H5 端使用 wap.pay 唤起支付宝 APP

前端传递 is_mobile 参数,AlipayProvider 根据设备类型选择:
- PC: alipay.trade.page.pay (FAST_INSTANT_TRADE_PAY)
- H5: alipay.trade.wap.pay (QUICK_WAP_WAY)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erio
2026-03-06 18:04:11 +08:00
parent 94d25ddc31
commit d46793f072
6 changed files with 17 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ const createOrderSchema = z.object({
payment_type: z.string().min(1), payment_type: z.string().min(1),
src_host: z.string().max(253).optional(), src_host: z.string().max(253).optional(),
src_url: z.string().max(2048).optional(), src_url: z.string().max(2048).optional(),
is_mobile: z.boolean().optional(),
}); });
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -23,7 +24,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 }); return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 });
} }
const { user_id, amount, payment_type, src_host, src_url } = parsed.data; const { user_id, amount, payment_type, src_host, src_url, is_mobile } = parsed.data;
// Validate amount range // Validate amount range
if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) { if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) {
@@ -48,6 +49,7 @@ export async function POST(request: NextRequest) {
clientIp, clientIp,
srcHost: src_host, srcHost: src_host,
srcUrl: src_url, srcUrl: src_url,
isMobile: is_mobile,
}); });
// 不向客户端暴露 userName / userBalance 等隐私字段 // 不向客户端暴露 userName / userBalance 等隐私字段

View File

@@ -255,6 +255,7 @@ function PayContent() {
payment_type: paymentType, payment_type: paymentType,
src_host: srcHost, src_host: srcHost,
src_url: srcUrl, src_url: srcUrl,
is_mobile: isMobile,
}), }),
}); });

View File

@@ -27,18 +27,18 @@ function assertAlipayEnv(env: ReturnType<typeof getEnv>) {
} }
/** /**
* 生成电脑网站支付的跳转 URLGET 方式) * 生成支付宝网站/H5支付的跳转 URLGET 方式)
* 用于 alipay.trade.page.pay * PC: alipay.trade.page.pay H5: alipay.trade.wap.pay
*/ */
export function pageExecute( export function pageExecute(
bizContent: Record<string, unknown>, bizContent: Record<string, unknown>,
options?: { notifyUrl?: string; returnUrl?: string }, options?: { notifyUrl?: string; returnUrl?: string; method?: string },
): string { ): string {
const env = assertAlipayEnv(getEnv()); const env = assertAlipayEnv(getEnv());
const params: Record<string, string> = { const params: Record<string, string> = {
...getCommonParams(env.ALIPAY_APP_ID), ...getCommonParams(env.ALIPAY_APP_ID),
method: 'alipay.trade.page.pay', method: options?.method || 'alipay.trade.page.pay',
biz_content: JSON.stringify(bizContent), biz_content: JSON.stringify(bizContent),
}; };

View File

@@ -22,16 +22,20 @@ export class AlipayProvider implements PaymentProvider {
}; };
async createPayment(request: CreatePaymentRequest): Promise<CreatePaymentResponse> { async createPayment(request: CreatePaymentRequest): Promise<CreatePaymentResponse> {
const method = request.isMobile ? 'alipay.trade.wap.pay' : 'alipay.trade.page.pay';
const productCode = request.isMobile ? 'QUICK_WAP_WAY' : 'FAST_INSTANT_TRADE_PAY';
const url = pageExecute( const url = pageExecute(
{ {
out_trade_no: request.orderId, out_trade_no: request.orderId,
product_code: 'FAST_INSTANT_TRADE_PAY', product_code: productCode,
total_amount: request.amount.toFixed(2), total_amount: request.amount.toFixed(2),
subject: request.subject, subject: request.subject,
}, },
{ {
notifyUrl: request.notifyUrl, notifyUrl: request.notifyUrl,
returnUrl: request.returnUrl, returnUrl: request.returnUrl,
method,
}, },
); );

View File

@@ -19,6 +19,7 @@ export interface CreateOrderInput {
clientIp: string; clientIp: string;
srcHost?: string; srcHost?: string;
srcUrl?: string; srcUrl?: string;
isMobile?: boolean;
} }
export interface CreateOrderResult { export interface CreateOrderResult {
@@ -134,6 +135,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
paymentType: input.paymentType, paymentType: input.paymentType,
subject: `${env.PRODUCT_NAME} ${payAmount.toFixed(2)} CNY`, subject: `${env.PRODUCT_NAME} ${payAmount.toFixed(2)} CNY`,
clientIp: input.clientIp, clientIp: input.clientIp,
isMobile: input.isMobile,
}); });
await prisma.order.update({ await prisma.order.update({

View File

@@ -21,6 +21,8 @@ export interface CreatePaymentRequest {
notifyUrl?: string; notifyUrl?: string;
returnUrl?: string; returnUrl?: string;
clientIp?: string; clientIp?: string;
/** 是否来自移动端(影响支付宝选择 PC 页面支付 / H5 手机网站支付) */
isMobile?: boolean;
} }
/** Response from creating a payment */ /** Response from creating a payment */