微信网页支付(现在通常指 JSAPI 支付)是用户在微信内置浏览器或微信小程序中访问网页时,直接调用微信支付完成支付的一种方式,其核心流程是:后端服务器生成预支付订单,然后返回一个包含支付所需参数的 JSON 给前端,前端通过微信 JS-SDK 的 chooseWXPay 方法调起微信支付收银台。

PHP开发微信网页支付接口
(图片来源网络,侵删)

整个开发过程分为以下几个关键步骤:

  1. 准备工作
  2. 后端 PHP 开发(核心逻辑)
  3. 前端页面开发(调起支付)
  4. 支付结果通知处理
  5. 查询订单状态

第一步:准备工作(至关重要)

在开始编码之前,你必须完成以下配置,否则一切免谈。

  1. 申请微信支付商户号

    • 访问 微信支付官网,注册并完成认证,获取一个 商户号(MCHID)
  2. 获取 API 密钥

    • 登录微信商户平台,进入「账户中心」->「API安全」->「API密钥(32位)」。
    • 设置并获取你的 APIv3 密钥(强烈建议使用 v3 版本,v2 已逐渐淘汰),这个密钥用于后续的签名和回调数据解密。
  3. 获取支付证书

    • 同样在「API安全」->「证书管理」中,下载 平台证书商户证书
    • 平台证书:用于验证微信支付回调通知的签名。
    • 商户证书:部分 API 调用可能需要,但 JSAPI 支付主要是用 APIv3 密钥。
    • 将下载的证书(.pem 文件)放在你项目的安全目录中,不要暴露给公网。
  4. 获取 AppID 和 OpenID

    • AppID:你的微信公众号或小程序的唯一标识。
    • OpenID:用户在你的公众号或小程序下的唯一标识。这是 JSAPI 支付的必需参数!
    • 如何获取 OpenID?
      • 用户通过微信浏览器访问你的网页。
      • 你需要引导用户进行 OAuth2.0 授权。
      • 授权成功后,微信服务器会回调到你指定的 URL,并在 URL 参数中带回 code
      • 使用这个 code,通过后端服务器向微信服务器请求,即可换取到用户的 openidaccess_token
  5. 配置支付目录

    • 在微信商户平台「产品中心」->「开发配置」->「支付配置」中,配置你的 JSAPI 支付授权目录。
    • 必须配置,否则用户点击支付时会提示“无效商户”或“目录未配置”,配置格式为 https://www.yourdomain.com/,需要包含完整的协议和域名,并且是精确到目录的(https://www.yourdomain.com/pay/)。

第二步:后端 PHP 开发(核心逻辑)

我们将使用 Composer 来管理依赖,推荐使用官方推荐的 wechatpay-v3 PHP SDK。

安装 SDK

在你的项目根目录下,通过 Composer 安装:

composer require wechatpay/wechatpay-v3

编写支付逻辑 (create_payment.php)

这个文件负责接收前端传来的订单信息,生成预支付订单,并返回支付参数。

<?php
// 引入自动加载文件
require 'vendor/autoload.php';
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use GuzzleHttp\Client;
// ==================== 1. 配置信息 ====================
// 从微信商户平台获取
$mchid = '你的商户号'; // 商户号
$serial_no = '你的平台证书序列号'; // API证书的序列号,可在商户平台查看
$private_key_path = '/path/to/your/apiclient_key.pem'; // 你的商户私钥文件路径
$api_v3_key = '你的APIv3密钥'; // APIv3密钥
// 从公众号获取
$appid = '你的AppID'; // 公众号或小程序的AppID
// 从数据库或缓存中获取
$openid = '用户的openid'; // 必须参数
// ==================== 2. 构建请求客户端 ====================
$instance = Builder::factory([
    'mchid'      => $mchid,
    'serial_no'  => $serial_no,
    'private_key'=> Rsa::from($private_key_path, Rsa::KEY_TYPE_FILE), // 使用文件路径加载私钥
    'certs'      => [ // 平台证书,用于验证回调签名
        $serial_no => Rsa::from('/path/to/your/wechatpay_*.pem', Rsa::KEY_TYPE_FILE), // 平台证书文件路径
    ],
]);
// ==================== 3. 准备支付请求参数 ====================
// 随机字符串
$nonce_str = md5(uniqid(mt_rand(), true));
// 时间戳
$time_stamp = (string)time();
// 订单号(确保在你的系统中唯一)
$out_trade_no = 'WXPHP' . date('YmdHis') . mt_rand(1000, 9999);
// 订单金额,单位:分
$total_fee = 100; // 1.00元
// 构建请求 body
$payload = [
    'appid'     => $appid,
    'mchid'     => $mchid,
    'description' => 'PHP网页支付测试订单',
    'out_trade_no' => $out_trade_no,
    'notify_url' => 'https://www.yourdomain.com/wechatpay_notify.php', // 支付结果通知地址,必须公网可访问
    'payer'     => ['openid' => $openid],
    'amount'    => [
        'total' => $total_fee,
        'currency' => 'CNY'
    ]
];
// ==================== 4. 发起请求,创建预支付订单 ====================
try {
    $response = $instance->chain('v3/pay/transactions/jsapi')
                          ->post(['json' => $payload]);
    $result = json_decode($response->getBody(), true);
    // ==================== 5. 生成前端调起支付所需的参数 ====================
    // 从响应中获取 prepay_id
    $prepay_id = $result['prepay_id'];
    // 再次构建前端支付参数
    $jsapi_payload = [
        'appId'     => $appid,
        'timeStamp' => $time_stamp,
        'nonceStr'  => $nonce_str,
        'package'   => 'prepay_id=' . $prepay_id,
    ];
    // 生成签名
    $jsapi_payload['signType'] = 'RSA';
    $jsapi_payload['paySign'] = Rsa::sign(
        Formatter::joinedByLineFeed(...array_values($jsapi_payload)),
        $private_key_path,
        Rsa::KEY_TYPE_FILE
    );
    // 返回成功结果给前端
    echo json_encode([
        'code' => 0,
        'msg'  => 'success',
        'data' => $jsapi_payload
    ]);
} catch (\Exception $e) {
    // 请求失败,返回错误信息
    echo json_encode([
        'code' => $e->getCode(),
        'msg'  => $e->getMessage(),
        'data' => []
    ]);
    // 在实际项目中,这里应该记录日志
    // error_log("创建预支付订单失败: " . $e->getMessage());
}

第三步:前端页面开发 (pay.php)

这个页面负责获取用户 openid,然后调用后端接口生成支付参数,并调起微信支付。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">微信支付测试</title>
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
    <h1>微信网页支付测试</h1>
    <button id="payButton" onclick="onPayButtonClick()">立即支付 1.00元</button>
    <script>
        // 在实际项目中,openid 应该通过后端 OAuth2.0 授权流程获取
        // 这里为了演示,我们假设已经获取到了
        // let openid = '通过授权流程获取到的用户openid';
        // 模拟一个获取 openid 的过程(实际中你的后端会做这件事)
        async function getOpenid() {
            // 这里应该是一个 AJAX 请求,调用你自己的后端接口
            // 该接口会完成微信 OAuth2.0 授权并返回 openid
            //  const response = await fetch('/api/get_openid.php');
            // const data = await response.json();
            // return data.openid;
            // --- 模拟数据 ---
            // 在真实环境中,请替换为实际的获取逻辑
            return new Promise((resolve) => {
                // 模拟网络延迟
                setTimeout(() => {
                    resolve('用户的openid'); // 替换为真实的openid
                }, 500);
            });
        }
        async function onPayButtonClick() {
            const payButton = document.getElementById('payButton');
            payButton.disabled = true;
            payButton.innerText = '处理中...';
            try {
                // 1. 获取用户的 openid
                const openid = await getOpenid();
                if (!openid) {
                    throw new Error('获取用户openid失败');
                }
                // 2. 调用后端 PHP 接口,生成预支付订单
                const response = await fetch('https://www.yourdomain.com/create_payment.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        // 可以在这里传递订单金额等动态信息
                        // amount: 100, // 1.00元
                        openid: openid
                    })
                });
                const result = await response.json();
                if (result.code !== 0) {
                    throw new Error(result.msg || '创建订单失败');
                }
                // 3. 调起微信支付
                const payParams = result.data;
                WeixinJSBridge.invoke('getBrandWCPayRequest', {
                    "appId":     payParams.appId,
                    "timeStamp": payParams.timeStamp,
                    "nonceStr":  payParams.nonceStr,
                    "package":   payParams.package,
                    "signType":  payParams.signType,
                    "paySign":   payParams.paySign
                }, function(res) {
                    if (res.err_msg === "get_brand_wcpay_request:ok") {
                        // 支付成功
                        alert('支付成功!');
                        // 可以在这里跳转到成功页面
                    } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
                        // 用户取消支付
                        alert('您已取消支付');
                    } else {
                        // 支付失败
                        alert('支付失败:' + res.err_msg);
                    }
                    // 恢复按钮状态
                    payButton.disabled = false;
                    payButton.innerText = '立即支付 1.00元';
                });
            } catch (error) {
                alert(error.message);
                payButton.disabled = false;
                payButton.innerText = '立即支付 1.00元';
            }
        }
        // 微信JS-SDK注入
        function onBridgeReady() {
            // JS-SDK 相关逻辑可以在这里初始化
        }
        if (typeof WeixinJSBridge == "undefined") {
            if (document.addEventListener) {
                document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
            } else if (document.attachEvent) {
                document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
            }
        } else {
            onBridgeReady();
        }
    </script>
</body>
</html>

第四步:支付结果通知处理 (wechatpay_notify.php)

当用户支付成功或失败后,微信服务器会主动向你在 create_payment.php 中设置的 notify_url 发送一个 POST 请求,你的服务器必须正确处理这个通知,并返回特定的响应。

<?php
require 'vendor/autoload.php';
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use GuzzleHttp\Client;
// ==================== 1. 配置信息 (与创建订单时相同) ====================
$mchid = '你的商户号';
$serial_no = '你的平台证书序列号';
$private_key_path = '/path/to/your/apiclient_key.pem';
$api_v3_key = '你的APIv3密钥';
// ==================== 2. 验证签名和回调数据 ====================
// 获取微信发送的签名头
$wechatpay_signature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_WECHATPAY_NONCE'] ?? '';
// 获取原始请求体
$body = file_get_contents('php://input');
// 使用平台证书验证签名
$verified = Rsa::verify(
    $timestamp . "\n" . $nonce . "\n" . $body . "\n",
    $wechatpay_signature,
    Rsa::from('/path/to/your/wechatpay_*.pem', Rsa::KEY_TYPE_FILE) // 平台证书
);
if (!$verified) {
    // 签名验证失败
    http_response_code(403);
    echo 'FAIL';
    exit();
}
// ==================== 3. 解析通知数据 ====================
$notification = json_decode($body, true);
$resource = $notification['resource'];
// 解密 resource 中的数据
$ciphertext = $resource['ciphertext'];
$associated_data = $resource['associated_data'];
$nonce = $resource['nonce'];
$decrypted_data = Rsa::aes256GcmDecrypt(
    $api_v3_key,
    $ciphertext,
    $associated_data,
    $nonce
);
$decrypted_payload = json_decode($decrypted_data, true);
// ==================== 4. 处理业务逻辑 ====================
$out_trade_no = $decrypted_payload['out_trade_no']; // 你自己的订单号
$transaction_id = $decrypted_payload['transaction_id']; // 微信支付订单号
$trade_state = $decrypted_payload['trade_state']; // 交易状态
if ($trade_state === 'SUCCESS') {
    // 1. 验证订单金额是否一致(非常重要!防止金额篡改)
    $callback_total_fee = $decrypted_payload['amount']['total'];
    // 从你的数据库中查询该订单的原始金额
    $original_total_fee = 100; // 假设从数据库查到是100分
    if ($callback_total_fee != $original_total_fee) {
        // 金额不一致,应记录异常并处理
        error_log("订单 {$out_trade_no} 回调金额异常!回调金额:{$callback_total_fee},原始金额:{$original_total_fee}");
        // 即使金额不对,也要告诉微信收到了,防止微信重复通知
        // 但业务上不应认为支付成功
    }
    // 2. 更新你自己的数据库订单状态为“已支付”
    // $order = Order::find($out_trade_no);
    // $order->status = 'paid';
    // $order->transaction_id = $transaction_id;
    // $order->save();
    // 业务处理代码...
    echo "订单 {$out_trade_no} 支付成功!";
} else {
    // 支付失败或处理中
    // $order = Order::find($out_trade_no);
    // $order->status = $trade_state; // CLOSED, REVOKED
    // $order->save();
    // 业务处理代码...
    echo "订单 {$out_trade_no} 支付状态:{$trade_state}";
}
// ==================== 5. 返回成功响应给微信 ====================
// 必须返回一个包含 success 字段的 JSON
http_response_code(200);
echo json_encode(['code' => 'SUCCESS', 'message' => '成功']);

第五步:查询订单状态

由于网络等原因,支付通知可能会延迟或丢失,在用户支付完成后,前端可以主动查询订单状态,以获取最实时的结果。

<?php
// query_order.php
require 'vendor/autoload.php';
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
// 配置信息...
$instance = Builder::factory([...]);
$out_trade_no = $_GET['out_trade_no'] ?? '';
if (!$out_trade_no) {
    echo json_encode(['code' => 1, 'msg' => '订单号不能为空']);
    exit;
}
try {
    $response = $instance->chain('v3/pay/transactions/out-trade-no/' . $out_trade_no . '?mchid=' . $mchid)
                          ->get();
    $result = json_decode($response->getBody(), true);
    $trade_state = $result['trade_state'];
    echo json_encode([
        'code' => 0,
        'msg'  => 'success',
        'data' => [
            'trade_state' => $trade_state,
            'trade_state_desc' => getStateDescription($trade_state)
        ]
    ]);
} catch (\Exception $e) {
    echo json_encode(['code' => $e->getCode(), 'msg' => $e->getMessage()]);
}
function getStateDescription($state) {
    $map = [
        'NOTPAY' => '未支付',
        'SUCCESS' => '支付成功',
        'REFUND' => '转入退款',
        'CLOSED' => '已关闭',
        'REVOKED' => '已撤销(付款码支付)',
        'USERPAYING' => '用户支付中',
        'PAYERROR' => '支付失败(其他原因,如银行返回失败)',
    ];
    return $map[$state] ?? '未知状态';
}

总结与最佳实践

  1. 安全第一:永远不要将 APIv3 密钥、商户私钥等敏感信息暴露在前端或代码仓库中,建议使用环境变量(如 .env 文件)来管理。
  2. 幂等性:处理支付回调时,必须根据 out_trade_no 来处理业务,并确保多次收到相同的回调不会产生重复的业务逻辑(重复发货),可以在数据库中记录已处理的 out_trade_notransaction_id
  3. 金额校验:在回调中,务必校验微信通知中的金额是否与你的订单金额一致,这是风控的关键环节。
  4. 异步通知:核心业务逻辑(如更新数据库、发货)应该在支付通知回调中完成,而不是在前端跳转后完成,以保证数据的最终一致性。
  5. 错误处理:网络请求、签名验证、数据库操作等任何环节都可能出错,必须有完善的错误捕获和日志记录机制。
  6. 测试:在正式环境前,务必使用微信支付的 沙箱环境 进行完整流程的测试,确保一切正常后再切换到生产环境。