PAYATHON 2026

如何使用 PHP 接入支付宝自研支付项目?

支付阿杰

结论

PHP 接入支付宝支付的流程包括:在支付宝开放平台创建应用并开通支付产品,配置应用密钥或证书,由服务端生成支付请求,接收异步通知并验签,再通过主动查单确认支付结果。

支付密钥只能保存在 PHP 服务端,不能写入 JavaScript、App 或公开仓库。业务系统也不能仅凭浏览器跳转结果将订单标记为已支付,必须以验签成功的异步通知或主动查单结果为准。

下面以“电脑网站支付”的密钥模式为例。手机网站支付、App 支付和当面付的整体流程相近,区别主要在调用的 API 和前端唤起方式。

接入前的准备

先在支付宝开放平台完成这些配置:

  1. 创建应用,取得 app_id。
  2. 根据业务场景开通对应产品,例如电脑网站支付或手机网站支付。
  3. 生成 RSA2 密钥对,将应用公钥上传到开放平台。
  4. 保存支付宝公钥,用它验证支付宝返回的数据。
  5. 配置异步通知地址 notify_url。
  6. 开发阶段可以使用沙箱环境,正式上线时再切换到生产网关和生产应用配置。

这几种密钥的用途不同:

  • 应用私钥由商户自行保管,用于给请求签名。
  • 支付宝公钥由支付宝提供,用于验证支付宝通知。
  • 应用公钥需要上传给支付宝,但不能用来验证支付宝通知。

如果应用采用证书模式,还需要准备应用公钥证书、支付宝公钥证书和支付宝根证书。证书模式的请求参数和验签过程与普通公钥模式不同,应使用对应模式的官方 SDK,不能直接套用下面的密钥模式代码。

配置可以放在环境变量或服务器密钥管理系统中:

ALIPAY_APP_ID=2021000000000000
ALIPAY_PRIVATE_KEY_PATH=/secure/path/app_private_key.pem
ALIPAY_PUBLIC_KEY_PATH=/secure/path/alipay_public_key.pem
ALIPAY_NOTIFY_URL=https://pay.example.com/alipay/notify
ALIPAY_RETURN_URL=https://www.example.com/payment/result
ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do

沙箱环境和生产环境使用不同的 app_id、密钥、买家账号及网关,不能混用。

发起电脑网站支付

电脑网站支付通常调用 alipay.trade.page.pay。下面的示例不依赖具体 SDK 版本,直接演示请求签名和表单提交过程。

<?php

declare(strict_types=1);

$config = [
    'app_id' => getenv('ALIPAY_APP_ID'),
    'private_key_path' => getenv('ALIPAY_PRIVATE_KEY_PATH'),
    'notify_url' => getenv('ALIPAY_NOTIFY_URL'),
    'return_url' => getenv('ALIPAY_RETURN_URL'),
    'gateway' => getenv('ALIPAY_GATEWAY') ?: 'https://openapi.alipay.com/gateway.do',
];

function loadPrivateKey(string $path): OpenSSLAsymmetricKey
{
    $content = file_get_contents($path);

    if ($content === false) {
        throw new RuntimeException('无法读取支付宝应用私钥');
    }

    $key = openssl_pkey_get_private($content);

    if ($key === false) {
        throw new RuntimeException('支付宝应用私钥格式错误');
    }

    return $key;
}

function buildSignContent(array $params): string
{
    unset($params['sign']);

    $params = array_filter(
        $params,
        static fn ($value) => $value !== '' && $value !== null
    );

    ksort($params);

    $pairs = [];
    foreach ($params as $key => $value) {
        $pairs[] = $key . '=' . $value;
    }

    return implode('&', $pairs);
}

function rsa2Sign(array $params, OpenSSLAsymmetricKey $privateKey): string
{
    $content = buildSignContent($params);

    if (!openssl_sign($content, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
        throw new RuntimeException('支付宝请求签名失败');
    }

    return base64_encode($signature);
}

$orderNo = 'PAY' . date('YmdHis') . random_int(100000, 999999);

/*
 * 实际项目中应先把订单写入数据库。
 * 金额必须从服务端商品数据计算,不能直接相信浏览器传入的金额。
 */
$bizContent = [
    'out_trade_no' => $orderNo,
    'total_amount' => '99.00',
    'subject' => '测试订单',
    'product_code' => 'FAST_INSTANT_TRADE_PAY',
];

$params = [
    'app_id' => $config['app_id'],
    'method' => 'alipay.trade.page.pay',
    'format' => 'JSON',
    'charset' => 'utf-8',
    'sign_type' => 'RSA2',
    'timestamp' => date('Y-m-d H:i:s'),
    'version' => '1.0',
    'notify_url' => $config['notify_url'],
    'return_url' => $config['return_url'],
    'biz_content' => json_encode(
        $bizContent,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
    ),
];

$privateKey = loadPrivateKey($config['private_key_path']);
$params['sign'] = rsa2Sign($params, $privateKey);
?>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>正在跳转到支付宝</title>
</head>
<body>
<form id="alipay-form"
      method="post"
      action="<?= htmlspecialchars($config['gateway'], ENT_QUOTES, 'UTF-8') ?>">
    <?php foreach ($params as $name => $value): ?>
        <input type="hidden"
               name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
               value="<?= htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') ?>">
    <?php endforeach; ?>
</form>

<script>
document.getElementById('alipay-form').submit();
</script>
</body>
</html>

生产环境中的订单号必须全局唯一。计算金额时建议使用字符串或最小货币单位,避免直接累加浮点数导致精度误差。

处理支付宝异步通知

用户完成支付后,支付宝会向 notify_url 发送服务器通知。通知处理至少需要检查以下内容:

  1. RSA2 验签是否通过。
  2. app_id 是否与当前应用一致。
  3. out_trade_no 是否存在于本系统。
  4. total_amount 是否与订单应付金额完全一致。
  5. 收款方信息是否符合预期,例如 seller_id 是否正确。
  6. trade_status 是否为当前业务认可的成功状态。
  7. 重复收到同一通知时,是否会重复发货。

示例:

<?php

declare(strict_types=1);

function buildVerifyContent(array $params): string
{
    unset($params['sign'], $params['sign_type']);

    $params = array_filter(
        $params,
        static fn ($value) => $value !== '' && $value !== null
    );

    ksort($params);

    $pairs = [];
    foreach ($params as $key => $value) {
        $pairs[] = $key . '=' . $value;
    }

    return implode('&', $pairs);
}

function verifyAlipayNotify(array $params, string $publicKeyPath): bool
{
    $sign = $params['sign'] ?? '';

    if ($sign === '') {
        return false;
    }

    $publicKeyContent = file_get_contents($publicKeyPath);

    if ($publicKeyContent === false) {
        throw new RuntimeException('无法读取支付宝公钥');
    }

    $publicKey = openssl_pkey_get_public($publicKeyContent);

    if ($publicKey === false) {
        throw new RuntimeException('支付宝公钥格式错误');
    }

    $result = openssl_verify(
        buildVerifyContent($params),
        base64_decode($sign, true),
        $publicKey,
        OPENSSL_ALGO_SHA256
    );

    return $result === 1;
}

$params = $_POST;

try {
    if (!verifyAlipayNotify(
        $params,
        (string) getenv('ALIPAY_PUBLIC_KEY_PATH')
    )) {
        http_response_code(400);
        echo 'failure';
        exit;
    }

    if (($params['app_id'] ?? '') !== getenv('ALIPAY_APP_ID')) {
        http_response_code(400);
        echo 'failure';
        exit;
    }

    $orderNo = $params['out_trade_no'] ?? '';
    $tradeNo = $params['trade_no'] ?? '';
    $tradeStatus = $params['trade_status'] ?? '';
    $paidAmount = $params['total_amount'] ?? '';

    /*
     * 从数据库读取订单,并核对:
     * 1. 订单存在;
     * 2. paidAmount 与数据库中的应付金额一致;
     * 3. seller_id 等收款方信息正确;
     * 4. 订单尚未被其他支付流水占用。
     */

    if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
        /*
         * 在数据库事务中执行幂等更新:
         * - 将订单更新为已支付;
         * - 保存 tradeNo;
         * - 记录原始通知;
         * - 触发后续发货或充值流程。
         *
         * 如果订单已经处理过,应直接视为成功,不能重复发货。
         */
    }

    echo 'success';
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
    echo 'failure';
}

只有业务处理成功后,接口才能输出纯文本 success。如果响应超时,或返回其他内容、failure,支付宝可能按规则再次发送通知,因此通知接口必须具备幂等性。

通知接口前不能设置登录验证、验证码或浏览器 Cookie 校验,同时要保证公网可以通过 HTTPS 正常访问该地址。

主动查询订单

异步通知可能因网络、网关或服务器故障而延迟。订单长时间停留在“支付中”状态时,应调用 alipay.trade.query 主动查询。

查询时通常传入商户订单号:

$bizContent = [
    'out_trade_no' => $orderNo,
];

$params = [
    'app_id' => $config['app_id'],
    'method' => 'alipay.trade.query',
    'format' => 'JSON',
    'charset' => 'utf-8',
    'sign_type' => 'RSA2',
    'timestamp' => date('Y-m-d H:i:s'),
    'version' => '1.0',
    'biz_content' => json_encode(
        $bizContent,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
    ),
];

$params['sign'] = rsa2Sign(
    $params,
    loadPrivateKey($config['private_key_path'])
);

将参数通过 POST 提交到支付宝网关后,还要验证网关响应签名,再根据查询结果更新订单。浏览器访问 return_url 只说明用户返回了商户页面,并不代表付款一定成功。

其他支付场景

不同支付方式的主要区别是接口名称和支付结果的交付方式:

  • 电脑网站支付使用 alipay.trade.page.pay,由服务端生成跳转表单。
  • 手机网站支付使用 alipay.trade.wap.pay,由服务端生成手机网页支付表单。
  • App 支付使用 alipay.trade.app.pay。PHP 服务端生成签名后的订单字符串,再由客户端调用支付宝客户端 SDK。
  • 当面付扫码通常由服务端调用预创建交易接口,取得二维码内容后展示给用户。

具体参数取决于已开通产品、签约状态、SDK 版本,以及采用密钥模式还是证书模式。使用支付宝官方 PHP SDK 时,应按照当前开放平台文档安装,并使用 SDK 提供的签名、验签和请求客户端。不要混用 SDK 签名与自定义签名代码。

常见问题和注意事项

  • PHP 环境需要启用 OpenSSL 扩展,并支持 SHA-256。
  • 私钥文件不能放在 Web 可访问目录,也不能提交到 Git。
  • notify_url 必须是支付宝服务器能够访问的完整地址,本机 localhost 无法直接接收通知。
  • 签名前不要对参数值进行 URL 编码。提交 HTTP 请求时,再由表单或客户端编码。
  • 验签必须使用支付宝公钥,不能误用应用公钥。
  • 数据库存储金额时,建议使用 DECIMAL,或者统一将“分”存为整数。
  • 订单状态更新、支付流水写入和库存处理应放在事务中,并通过唯一索引或状态条件保证幂等。
  • 支付宝传回的 subject、金额或订单号不能直接当作可信的业务数据。
  • 正式上线前需要测试支付成功、取消支付、重复通知、通知乱序、金额不一致、验签失败、查单补偿和退款等场景。
  • 支付宝开放平台的产品权限、SDK 结构和证书要求可能调整,最终应以当前应用后台显示的能力及对应 API 文档为准。

备注:内容仅供参考。