PAYATHON 2026

ThinkPHP 8.0开发支付宝手机支付H5 API应选择哪个PHP SDK?

支付阿杰

结论

使用 ThinkPHP 8.0 和 PHP 8.0 开发支付宝 H5 手机网站支付时,建议优先选择支付宝通用版 PHP SDK,调用 alipay.trade.wap.pay。如果支付宝开放平台当前提供的通用版 v3 SDK 已明确支持该接口,并且项目准备统一采用 v3 协议,也可以选择 v3。否则,成熟的通用版 v2 SDK 用起来更省事。

新项目不建议继续花费大量时间排查 Easy SDK 的兼容问题。Easy SDK 虽然可以完成 H5 支付,但封装层较高,遇到签名、证书或异步通知问题时,排查起来不如通用版 SDK 直观。无论使用哪个 SDK,最终支付结果都要以服务端收到的异步通知为准。

还要注意,“v2”和”v3”既可能指接口协议版本,也可能指 SDK 包装版本。不要只看名称,需要确认 SDK 是否直接支持 alipay.trade.wap.pay,以及应用使用的是普通公钥模式还是证书模式。

推荐的实现方式

H5 支付通常按以下流程处理:

  1. 服务端创建 alipay.trade.wap.pay 请求。
  2. 将支付宝返回的跳转表单或 URL 交给浏览器。
  3. 用户在支付宝页面完成付款。
  4. 支付宝向 notify_url 发送异步通知。
  5. 服务端验签成功后,再核对订单号、金额、应用 ID 和卖家身份。
  6. 订单更新成功后返回纯文本 success。

return_url 只是浏览器的同步跳转地址,不能用来最终判断订单是否支付成功。

下面是通用版 SDK 的典型写法。不同发行包的命名空间、加载路径和部分属性可能略有不同,请以项目实际安装的 SDK 为准。

<?php

use Alipay\AopClient;
use Alipay\Request\AlipayTradeWapPayRequest;

$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = '你的 APPID';
$aop->rsaPrivateKey = '应用私钥内容';
$aop->alipayrsaPublicKey = '支付宝公钥内容';
$aop->signType = 'RSA2';
$aop->charset = 'UTF-8';
$aop->format = 'json';

$request = new AlipayTradeWapPayRequest();

$request->setBizContent(json_encode([
    'out_trade_no' => 'ORDER_202609090001',
    'total_amount' => '99.00',
    'subject' => '订单支付',
    'product_code' => 'QUICK_WAP_WAY',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

$request->setNotifyUrl('https://example.com/alipay/notify');
$request->setReturnUrl('https://example.com/alipay/return');

$html = $aop->pageExecute($request, 'GET');

return response($html)->contentType('text/html');

金额最好使用保留两位小数的字符串,不要让浮点数直接参与金额计算。订单号必须由服务端生成,并保证唯一。

“验签失败”的重点排查顺序

1. 是否配置了错误的公钥

验签需要使用”支付宝公钥”,不能使用:

  • 应用公钥;
  • 应用私钥;
  • 支付宝公钥证书中的任意一段文本;
  • 从其他应用或沙箱环境复制的公钥。

应用私钥负责为请求签名,支付宝公钥用于验证支付宝的响应。沙箱和正式环境各自对应一套密钥、APPID 和网关,不能混用。

如果应用采用证书模式,需要配置应用公钥证书、支付宝公钥证书和支付宝根证书,不能再按普通公钥模式处理。

2. 是否验错了数据

异步通知验签必须使用支付宝发送的原始 POST 参数。排除 sign 和 sign_type 后,再按 SDK 的规则验签。不要先把参数转换成业务对象,也不要只取 trade_no、out_trade_no 等部分字段进行验签。

<?php

$post = request()->post();

$signType = $post['sign_type'] ?? 'RSA2';

// 具体参数形式可能因通用版 SDK 发行版本而不同。
// 如果公钥已配置到 AopClient,通常可让 SDK 使用当前配置验签。
$verified = $aop->rsaCheckV1($post, null, $signType);

if (!$verified) {
    return response('failure', 200);
}

有些 SDK 版本要求 rsaCheckV1() 的第二个参数传入公钥文件路径,而不是 null。如果方法签名不同,应按照当前安装版本的定义传参,不要直接照搬其他版本的示例。

3. 不要再次 URL 解码通知参数

Web 框架通常已经解析并解码 application/x-www-form-urlencoded 请求。再次执行下面这些操作,很容易破坏待验签的内容:

urldecode($value);
base64_decode($value);
html_entity_decode($value);

尤其需要检查 PHP 或中间件是否把签名中的 + 转成了空格。调试时可以同时记录原始请求体和框架解析后的参数,但日志中的敏感信息需要脱敏。

4. 确认签名算法一致

支付宝请求一般使用 RSA2。应用配置、开放平台的密钥设置以及通知参数中的 sign_type 必须保持一致。

不要混用:

RSA

和:

RSA2

两者使用的摘要算法不同。

5. 检查私钥、公钥格式

PEM 密钥必须保留正确的头尾标记和换行,例如:

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

或者:

-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----

从环境变量读取密钥时,换行经常会被保存成字符 \n。遇到这种情况,需要在配置层将其还原为真实换行:

$privateKey = str_replace('\n', "\n", env('ALIPAY_PRIVATE_KEY'));

如果密钥中已经包含真实换行,就不要重复处理。

6. 检查 ThinkPHP 中间件

确认通知接口没有受到以下处理的干扰:

  • CSRF 校验;
  • 登录鉴权;
  • 请求参数过滤或自动转义;
  • 参数类型强制转换;
  • 自动返回 JSON;
  • 异常页面或调试输出。

支付宝通知接口完成验签和业务处理后,必须返回纯文本:

return response('success', 200)->contentType('text/plain');

处理失败时返回:

return response('failure', 200)->contentType('text/plain');

如果返回 HTML、JSON、空字符串或额外的调试信息,支付宝可能会认为通知处理失败,并继续重试。

Easy SDK 的处理建议

如果暂时继续使用 Easy SDK,先确认下面的配置来自同一个支付宝应用和同一套环境:

$options->protocol = 'https';
$options->gatewayHost = 'openapi.alipay.com';
$options->signType = 'RSA2';
$options->appId = '你的 APPID';
$options->merchantPrivateKey = '应用私钥';
$options->alipayPublicKey = '支付宝公钥';
$options->notifyUrl = 'https://example.com/alipay/notify';

然后调用 Easy SDK 自带的通知验签方法,完整传入未经业务修改的通知参数。不同 PHP 版本的 Easy SDK 可能使用不同的类名和方法签名,因此需要查看 Composer 当前实际安装版本中的方法定义,不要混用 Java、旧版 PHP 或通用版 SDK 的示例。

如果问题仍未解决,记录以下信息通常最有助于定位:

  • 当前 APPID;
  • 沙箱或正式环境;
  • 普通公钥模式或证书模式;
  • 通知中的 sign_type;
  • 原始请求体;
  • SDK 实际收到的参数;
  • 当前配置的支付宝公钥指纹或摘要。

不要把完整的应用私钥写入日志。

验签成功后还要做业务校验

验签通过只能说明通知大概率来自对应密钥的持有方,不能因此直接把当前订单改成已支付。至少还要检查:

if (($post['app_id'] ?? '') !== config('alipay.app_id')) {
    return response('failure', 200);
}

if (($post['out_trade_no'] ?? '') !== $order->order_no) {
    return response('failure', 200);
}

if (bccomp((string) ($post['total_amount'] ?? '0'), $order->amount, 2) !== 0) {
    return response('failure', 200);
}

if (!in_array($post['trade_status'] ?? '', [
    'TRADE_SUCCESS',
    'TRADE_FINISHED',
], true)) {
    return response('failure', 200);
}

订单更新必须具备幂等性。同一条通知可能发送多次,已经支付的订单不能重复扣库存、发货或记账。如果对通知信息有疑问,可以再调用支付宝交易查询接口,由服务端确认交易状态。

备注:内容仅供参考。