支付宝手机网站支付 alipay.trade.wap.pay 下单:product_code 写错不报错,pageExecute 的 GET 和 POST 别混
手机浏览器里点“支付宝支付”,页面跳转到支付宝收银台,这条链路对应手机网站支付,接口是 alipay.trade.wap.pay。下单这一步看起来只是拼参数、调 SDK,实际容易踩的坑集中在几个地方:product_code 有两代写法、pageExecute 返回表单还是链接、return_url 被误当成支付成功回调、异步通知地址在内网收不到。下面按下单侧的顺序整理一遍。
相关资料:
- 支付宝开放平台文档:
- PHP SDK 示例项目:
- learnku 对接文档:
- 官方技术支持示例代码:
先分清四个支付接口
同样是支付宝付款,不同终端对应不同接口,别混用:
- 手机浏览器 H5 页面唤起支付宝:手机网站支付,
alipay.trade.wap.pay - PC 网页:电脑网站支付,
alipay.trade.page.pay - 原生 App:App 支付,
alipay.trade.app.pay - 扫码、付款码:当面付
本文只讨论第一行。
网关地址:沙箱和生产是两套
- 沙箱:
https://openapi.alipaydev.com/gateway.do - 生产:
https://openapi.alipay.com/gateway.do
这两个地址不是可选项。沙箱的 app_id、密钥只能配沙箱网关,生产的只能配生产网关。
必传参数
alipay.trade.wap.pay 的业务参数里,以下几项是必传的:
| 参数 | 说明 |
|---|---|
out_trade_no | 商户订单号,业务侧唯一 |
total_amount | 金额,字符串,两位小数,例如 "0.01",不要用浮点数 |
subject | 订单标题 |
可选参数里比较常用的:
product_code:销售产品码,见下一节quit_url:用户中途退出支付时的回跳地址,wap 支付特有passback_params:回调时原样带回的透传参数extend_params:扩展参数,花呗分期在这里配置。hb_fq_num只支持 3、6、12;hb_fq_seller_percent为 100 表示商家承担手续费,为 0 表示用户承担
product_code:ALIPAY_WAP_PAY 还是 QUICK_WAP_WAY
这是下单侧最容易抄错的一个字段。同一个接口,两代示例里写法不一样:
- 官方技术支持中心的 Java 示例,注释里写的是
ALIPAY_WAP_PAY - PHP 示例里写的是
QUICK_WAP_WAY - 当前开放平台文档中,手机网站支付的销售产品码为
QUICK_WAP_WAY
两代示例并存,直接复制很容易拿到旧值。问题在于,写错不会立刻返回“参数错误”,请求能发出去,网关侧判为产品码不匹配,返回 isv.invalid-parameter 一类错误。排查时容易往签名、密钥方向找,实际上是 product_code 的值不对。
按当前文档统一用 QUICK_WAP_WAY。
pageExecute:POST 表单和 GET 链接是两种返回
PHP SDK 里,pageExecute($request) 返回一段自提交的 HTML 表单,浏览器加载后自动 POST 到网关:
$request = new AlipayTradeWapPayRequest();
$request->setNotifyUrl('https://example.com/notify');
$request->setReturnUrl('https://example.com/return');
$request->setBizContent(json_encode([
'out_trade_no' => $outTradeNo,
'total_amount' => '0.01',
'subject' => '测试商品',
'product_code' => 'QUICK_WAP_WAY',
'quit_url' => 'https://example.com/quit',
], JSON_UNESCAPED_UNICODE));
// 返回自提交 HTML 表单,POST 到网关
$form = $alipayClient->pageExecute($request);
加第二个参数 "GET",返回的是一条长链接:
// 返回 https://openapi.alipay.com/gateway.do?...&sign=... 形式的长链接
$url = $alipayClient->pageExecute($request, 'GET');
Java SDK 对应的是 alipayClient.pageExecute(request) 和 pageExecute(request, "GET")。
两者的区别不只是请求方法。表单适合直接输出到页面、由浏览器自动提交;GET 链接是一串带 sign 的 URL,可以自己控制跳转时机、写日志、放进二维码,或者交给前端处理。选哪种取决于页面怎么组织,但不要在同一处混着用。
quit_url 只负责“中途退出”
quit_url 是 wap 支付特有的参数:用户在支付宝收银台点返回、中途放弃支付时,跳回这个地址。它不代表支付结果,只是给用户一个回到商户页面的出口。
return_url 不能作为支付成功的依据
return_url 是同步跳转地址。支付完成后浏览器跳回商户页面,地址栏里带一串参数。这个跳转只用于展示,用户可以刷新、可以重放这个 URL,也可以自己伪造参数。
支付宝文档里的原话是:支付结果以异步通知为准,不能以同步返回为准,因为实际支付成功但断网断电导致页面未跳转时,收不到同步通知。
所以订单状态只能由两处驱动:
- 异步通知
notify_url收到后验签、校验金额、更新订单 - 主动调用
alipay.trade.query查询
return_url 页面最多用来给用户展示“支付处理中”,不写数据库状态。
notify_url 必须公网可 POST 访问
notify_url 有几个硬性条件:
- 公网可访问,能接收 POST
- 处理完成后返回 HTTP 200
- 不设置
notify_url就不会发送异步通知。支付宝文档写得很直接:notify_url参数不设置,即不会触发发送异步通知
localhost、127.0.0.1、192.168.x.x 这类地址支付宝侧访问不到,收不到通知。本地联调需要公网测试环境或内网穿透。用穿透时注意两点:穿透地址在调试期间要保持有效;部分穿透服务会改写 POST body 或参数编码,导致验签失败。
本地可以这样起服务,生成支付表单并跳到支付宝:
php -S 127.0.0.1:8000 -t public
页面能跳转、能付款,但异步通知仍然需要公网地址。
微信、钉钉内置浏览器唤起不了
在微信、钉钉等内置浏览器里打开 H5 页面,部分环境会限制跳转支付宝。这不是签名问题,也不是参数问题,接口层解决不了。能做的只有在页面上提示用户“请用系统浏览器打开”。这是产品层面的限制,排查时不要往 SDK 配置上找。
服务商模式:app_auth_token
服务商代商户收款时,需要带 app_auth_token。两种写法:
$request->putOtherTextParam('app_auth_token', $appAuthToken);
// 或者
$alipayClient->pageExecute($request, '', $appAuthToken);
带上之后,收款进入授权商户的账户,而不是服务商自己的账户。
沙箱与生产必须成套切换
这一条踩一次就够记一辈子。以下五项必须一起换:
app_id- 应用私钥
- 支付宝公钥
- 网关地址
- 回调域名
沙箱参数和正式参数分开保存,不要放在同一个配置文件里靠注释切换。混用的典型表现是签名错误,或者订单打到了错误的环境——沙箱下单成功、生产查不到,或者反过来。
签名错误先查这几项
遇到签名错误,按顺序排查:
- 应用私钥是否属于当前
app_id - 配置的是支付宝公钥,还是误配成了应用公钥
sign_type是否为RSA2charset是否为UTF-8- 私钥是否存在换行、空格损坏(粘贴时被编辑器折行很常见)
- 签名之后参数是否又被改动过
第 6 条容易被忽略:签名是对参数串算的,签名完再改任何一个参数值,验签必然失败。
交易状态与后续接口
异步通知里的交易状态有四种:
WAIT_BUYER_PAY:交易创建,等待付款TRADE_CLOSED:未付款超时关闭,或付款后退款完成关闭TRADE_SUCCESS:支付成功TRADE_FINISHED:交易结束,不可退款
异步通知默认触发 TRADE_CLOSED、TRADE_FINISHED、TRADE_SUCCESS 三种状态,WAIT_BUYER_PAY 默认不发。
下单之后的几个接口:
alipay.trade.query:主动查询订单状态alipay.trade.close:关闭未付款订单alipay.trade.refund:退款,参数refund_amount、out_request_no
out_request_no 必须唯一。同一笔订单多次退款时,如果重试复用了同一个值,会被当成同一次退款;业务侧要自己存退款记录,避免网络重试导致重复退款。
证书模式
使用证书模式时,需要三份证书:
- 应用公钥证书
- 支付宝公钥证书
- 支付宝根证书
EasySwoole 支付组件里通过 setCertMode(true) 开启。证书模式和普通公钥模式不要混用,配置项对不上同样会表现为签名错误。
下单侧的核心其实只有几件事:接口选对、product_code 用当前文档的值、pageExecute 想清楚要表单还是链接、支付结果只认异步通知和主动查询、沙箱和生产参数成套切换。剩下的报错,大多能从前面的参数和配置里找到原因。