fix: 修复微信支付 Native/H5 判断逻辑,改为前端设备检测驱动
- 修复 clientIp 始终存在导致永远走 H5 的 bug,改用 isMobile 判断 - 前端通过 detectDeviceIsMobile() 传 is_mobile 给后端 - ENABLED_PAYMENT_TYPES 默认改为空,必须显式配置才启用 - 补充 .env.example 配置说明
This commit is contained in:
@@ -29,6 +29,7 @@ EASY_PAY_RETURN_URL="https://pay.example.com/pay/result"
|
||||
#STRIPE_WEBHOOK_SECRET="whsec_..."
|
||||
|
||||
# ── 支付宝直连(PAYMENT_PROVIDERS 含 alipay 时必填) ────────────────────
|
||||
# 不在 PAYMENT_PROVIDERS 中配置 alipay 则不启用支付宝直连
|
||||
# ALIPAY_APP_ID=
|
||||
# ALIPAY_PRIVATE_KEY= # PKCS8 格式私钥(不含 -----BEGIN/END----- 头尾)
|
||||
# ALIPAY_PUBLIC_KEY= # 支付宝公钥(非应用公钥,从开放平台获取)
|
||||
@@ -36,6 +37,8 @@ EASY_PAY_RETURN_URL="https://pay.example.com/pay/result"
|
||||
# ALIPAY_RETURN_URL=https://pay.example.com/pay/result
|
||||
|
||||
# ── 微信支付直连(PAYMENT_PROVIDERS 含 wxpay 时必填) ────────────────────
|
||||
# 前端自动检测设备类型:PC 端使用 Native 扫码支付,移动端使用 H5 跳转微信 APP 支付
|
||||
# 不在 PAYMENT_PROVIDERS 中配置 wxpay 则不启用微信支付
|
||||
# WXPAY_APP_ID= # 公众号或移动应用 AppID
|
||||
# WXPAY_MCH_ID= # 商户号(10位数字)
|
||||
# WXPAY_PRIVATE_KEY= # 商户 API 私钥 PEM(含 -----BEGIN/END----- 头尾)
|
||||
@@ -45,8 +48,9 @@ EASY_PAY_RETURN_URL="https://pay.example.com/pay/result"
|
||||
# WXPAY_PUBLIC_KEY_ID= # 微信支付公钥 ID
|
||||
# WXPAY_NOTIFY_URL=https://pay.example.com/api/wxpay/notify
|
||||
|
||||
# ── 启用的支付渠道(在已配置服务商支持的渠道中选择) ─────────────────────────
|
||||
# ── 启用的支付渠道(必须显式配置,未列出的渠道不会展示给用户) ─────────────
|
||||
# 可选值: alipay, wxpay, stripe
|
||||
# 默认值为空 = 不启用任何渠道,必须手动配置
|
||||
ENABLED_PAYMENT_TYPES="alipay,wxpay"
|
||||
|
||||
# ── 订单配置 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -7,6 +7,7 @@ const createOrderSchema = z.object({
|
||||
user_id: z.number().int().positive(),
|
||||
amount: z.number().positive(),
|
||||
payment_type: z.enum(['alipay', 'wxpay', 'stripe']),
|
||||
is_mobile: z.boolean().optional(),
|
||||
src_host: z.string().max(253).optional(),
|
||||
src_url: z.string().max(2048).optional(),
|
||||
});
|
||||
@@ -21,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
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, is_mobile, src_host, src_url } = parsed.data;
|
||||
|
||||
// Validate amount range
|
||||
if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) {
|
||||
@@ -44,6 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
amount,
|
||||
paymentType: payment_type,
|
||||
clientIp,
|
||||
isMobile: is_mobile,
|
||||
srcHost: src_host,
|
||||
srcUrl: src_url,
|
||||
});
|
||||
|
||||
@@ -249,6 +249,7 @@ function PayContent() {
|
||||
user_id: effectiveUserId,
|
||||
amount,
|
||||
payment_type: paymentType,
|
||||
is_mobile: isMobile,
|
||||
src_host: srcHost,
|
||||
src_url: srcUrl,
|
||||
}),
|
||||
|
||||
@@ -55,12 +55,16 @@ const envSchema = z.object({
|
||||
STRIPE_PUBLISHABLE_KEY: optionalTrimmedString,
|
||||
STRIPE_WEBHOOK_SECRET: optionalTrimmedString,
|
||||
|
||||
// ── 启用的支付渠道(在已配置服务商支持的渠道中选择) ──
|
||||
// 易支付支持: alipay, wxpay;Stripe 支持: stripe
|
||||
// ── 启用的支付渠道(必须显式配置,未列出的渠道不会展示给用户) ──
|
||||
ENABLED_PAYMENT_TYPES: z
|
||||
.string()
|
||||
.default('alipay,wxpay')
|
||||
.transform((v) => v.split(',').map((s) => s.trim())),
|
||||
.default('')
|
||||
.transform((v) =>
|
||||
v
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
|
||||
ORDER_TIMEOUT_MINUTES: z.string().default('5').transform(Number).pipe(z.number().int().positive()),
|
||||
MIN_RECHARGE_AMOUNT: z.string().default('1').transform(Number).pipe(z.number().positive()),
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface CreateOrderInput {
|
||||
amount: number;
|
||||
paymentType: PaymentType;
|
||||
clientIp: string;
|
||||
isMobile?: boolean;
|
||||
srcHost?: string;
|
||||
srcUrl?: string;
|
||||
}
|
||||
@@ -144,6 +145,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
||||
notifyUrl,
|
||||
returnUrl,
|
||||
clientIp: input.clientIp,
|
||||
isMobile: input.isMobile,
|
||||
});
|
||||
|
||||
await prisma.order.update({
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface CreatePaymentRequest {
|
||||
notifyUrl?: string;
|
||||
returnUrl?: string;
|
||||
clientIp?: string;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
/** Response from creating a payment */
|
||||
|
||||
@@ -35,7 +35,10 @@ export class WxpayProvider implements PaymentProvider {
|
||||
throw new Error('WXPAY_NOTIFY_URL is required');
|
||||
}
|
||||
|
||||
if (request.clientIp) {
|
||||
if (request.isMobile) {
|
||||
if (!request.clientIp) {
|
||||
throw new Error('clientIp is required for H5 payment');
|
||||
}
|
||||
const h5Url = await createH5Order({
|
||||
out_trade_no: request.orderId,
|
||||
description: request.subject,
|
||||
|
||||
Reference in New Issue
Block a user