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:
@@ -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 等隐私字段
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,18 +27,18 @@ function assertAlipayEnv(env: ReturnType<typeof getEnv>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成电脑网站支付的跳转 URL(GET 方式)
|
* 生成支付宝网站/H5支付的跳转 URL(GET 方式)
|
||||||
* 用于 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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user