Bootstrap

使用wechatpay-php实现JSAPI支付(服务商和普通商户)


前言

之前我使用的sdk是“wechatpay-guzzle-middleware”这个,后面发现官方文档里面“wechatpay-php”后面带了个推荐,以前没用过这个,正好有新项目试一下。
下面是全部的支付流程代码,已经跑通,拉起支付了。支付回调还没测试,等测试完继续更新。

一、wechatpay-php下载

我用的是composer,简单。

composer require wechatpay/wechatpay

二、各种证书环境配置

1.商户号API证书

这个就去要用的商户号里面,下载,没设置的话设置一下就行了。

2.平台证书

这个比较难搞,官方文档给了一个代码示例,我没用,嫌麻烦。这个我记得有效期挺久的,我就下载了一个。
代码如下(官方文档):

// apiV3key:商户号v3秘钥
// mchId:商户号
// mchPrivateKeyFilePath:API证书私钥
// mchSerialNo:API证书序列号
// outputFilePath:保存证书文件的地址(默认在项目的public文件下,我这里设置的是“file:///www/wwwroot/项目名/public/cert/”)
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}

三、接入jsapi支付

1.环境配置

我是把所有的证书相关信息写在类初始化里面,直接写在方法里面也可以。
代码如下(示例):

  	// 商户私钥,文件路径假定为 `cert/apiclient_key.pem`
    // 我是把API证书私钥将整个文件放在cert文件夹下
    $merchantPrivateKeyFilePath = public_path() . 'cert/apiclient_key.pem';
    // 加载商户私钥
    $this->merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);

    // 商户证书,文件路径假定为 `cert/apiclient_cert.pem`
    // 我是把API证书公钥将整个文件放在cert文件夹下
    $merchantCertificateFilePath = public_path() . 'cert/apiclient_cert.pem';
    // 加载商户证书
    $this->merchantCertificateInstance = PemUtil::loadCertificate($merchantCertificateFilePath);
    // 解析商户证书序列号
    $this->merchantCertificateSerial = PemUtil::parseCertificateSerialNo($this->merchantCertificateInstance);

    // 平台证书,就是上一步下载的平台证书,我直接丢到cert文件夹了,它生成名字很长,大概长下面这样
    $platformCertificateFilePath = public_path() . 'cert/wechatpay_48E897EFB18EE05C97B4EEF20F64193xxxxxxxxx.pem';
    // 加载平台证书
    $this->platformCertificateInstance = PemUtil::loadCertificate($platformCertificateFilePath);
    // 解析平台证书序列号
    $this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateInstance);

2.请求jsapi接口前的参数准备等

$PaymentAmount = bcmul($pay_price, 100, 0); // 支付金额 单位:分
// 商户号,我是取的配置里面的服务商账号,你可以用自己的,写成固定值也可。普通商户号也是直接填,一样的。
$merchantId = $this->config['service_member'];

// 工厂方法构造一个实例
$instance = Builder::factory([
    'mchid' => $merchantId,
    'serial' => $this->merchantCertificateSerial, // 商户证书序列号用上了
    'privateKey' => $this->merchantPrivateKeyInstance, //商户私钥
    'certs' => [
        $this->platformCertificateSerial => $this->platformCertificateInstance, // 平台序列号和平台证书
    ],
]);

$params = [
    'sp_appid' => $this->config['wx_app_appid'],  // 微信小程序appid
    'sp_mchid' => $merchantId, // 服务商商户号
    'sub_mchid' => $sub_mchid, // 特约商户号,如果是直连商户号,根据api文档改下参数就行了
    'description' => '购买商品',
    'out_trade_no' => $out_trade_no, // 自己生成一个唯一的支付订单号
    'notify_url' => $this->config['wechat_notify'], //支付回调地址,我是写到配置里面了
    'settle_info' => [
        'profit_sharing' => true, // 这个是分账与否,开启会冻结资金,后续就可以做分账了。直连商户号无需填写 true/false
    ],
    'amount' => [
        'total' => (int)$PaymentAmount, // 单位:分
        'currency' => 'CNY'
    ],
    'payer' => [
        'sp_openid' => $weapp_open_id, // 支付用户在该小程序下的openid
    ]
];

$authorization = $this->getAuthorization("POST", json_encode($params, JSON_UNESCAPED_UNICODE));
// 这个是请求头的签名,新文档里面很清晰。getAuthorization()这个是我写的方法,下一步给到。

3.获取Authorization

(我对sdk理解有问题,这一步省略,无需自己生成签名,SDK自己生成了)。

public function getAuthorization($http_method = "POST", $body = [], $url = "v3/pay/partner/transactions/jsapi"): string
{
    $serial_no = $this->merchantCertificateSerial; // 商户证书序列号
    $mchid = $this->config['service_member'];  // 商户号
    $timestamp = time();
    $nonce = $this->nonce_str();
    $url_parts = parse_url($url);

    $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
    $message = $http_method . "\n" .
        $canonical_url . "\n" .
        $timestamp . "\n" .
        $nonce . "\n" .
        $body . "\n";

    openssl_sign($message, $raw_sign, $this->merchantPrivateKeyInstance, 'sha256WithRSAEncryption');
    $sign = base64_encode($raw_sign);

    return sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
        $mchid, $nonce, $timestamp, $serial_no, $sign);
}

4.请求jsapi接口

这一步有点坑,很多时候报错的$resp拿不到,直接就报错了。我都是把参数记录下来,然后直接在postman里面访问jsapi接口,看下到底返回什么。如果遇到了相同的问题,可以参考我的解决方案。

$resp = $instance
    ->chain('v3/pay/partner/transactions/jsapi') // 我这里是服务商(合作伙伴)的接口,直连商户去看下文档用哪个
    ->post([
        'json' => $params,
        //'headers' => [
          //  'Content-type' => 'application/json;charset=utf-8',
        //    'Accept' => 'application/json',
         //   'User-Agent' => '*/*',
         //   'Authorization' => $this->schema . " " . $authorization //这个schema是认证类型,默认值填这个'WECHATPAY2-SHA256-RSA2048'
        //],
    ]);

$result_code = $resp->getStatusCode();

5.生成支付签名

这一步是生成支付签名,前端调用就可以拉起微信JASPI支付了。

if ($result_code == 200) { // 根据返回状态判断下,如果不是200,去查看下原因
    $result_data = json_decode($resp->getBody(), true);
    $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyInstance); //商户私钥
    $arouse_data = [
        'appId' => $this->config['wx_app_appid'],  //微信小程序appid
        'timeStamp' => strval(Formatter::timestamp()),
        'nonceStr' => Formatter::nonce(),
        'package' => 'prepay_id=' . $result_data['prepay_id'], // 这个prepay_id是有有效期的,具体多少我忘了,我自己设置的30分钟,超了就重新请求
    ];

    $arouse_data += ['paySign' => Rsa::sign(
        Formatter::joinedByLineFeed(...array_values($arouse_data)),
        $merchantPrivateKeyInstance
    ), 'signType' => 'RSA'];
    $arouse_data['out_trade_no'] = $out_trade_no;
    return $this->setSuccess('success', $arouse_data); //返回签名给前端就可以了,setSuccess()这个是我封装的方法
}

6.支付回调

还没测试,后面测试了再来更新。

    public function weChatNotify()
    {
        $notifyData = file_get_contents('php://input');
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array)json_decode($notifyData, true);
        // 使用PHP7的数据解构语法,从Array中解构并赋值变量
        ['resource' => [
            'ciphertext' => $ciphertext,
            'nonce' => $nonce,
            'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = AesGcm::decrypt($ciphertext, config('wechatpay.apiv3'), $nonce, $aad);
        // 把解密后的文本转换为PHP Array数组
        $inBodyResourceArray = (array)json_decode($inBodyResource, true);
        if ($inBodyResourceArray['trade_state'] == "SUCCESS") {
            Db::startTrans();
            try {
                //编写逻辑
                $order_no = $inBodyResourceArray['out_trade_no'];
                $attach = json_decode($inBodyResourceArray['attach'], true); 

                Db::commit();
                json([
            		'return_code' => 'SUCCESS',
            		'return_msg' => 'OK',
        		])->send(); // 这个是返回接收成功信息
            } catch (Exception $e) {
                Db::rollback();
                return true;
            }
        } else {
            // 回调异常
            Log::write($inBodyResourceArray, 'error');
            file_put_contents("NotifyError.txt", var_export($inBodyResourceArray, 1));
        }
        return true;
    }

四、完整的jsapi支付代码,服务商版本

protected $config;
protected $merchantPrivateKeyInstance;
protected $merchantCertificateInstance;
protected $merchantCertificateSerial;
protected $platformCertificateInstance;
protected $platformCertificateSerial;
protected $schema = 'WECHATPAY2-SHA256-RSA2048';

public function __construct()
{
    $this->config = $this->getConfigGroup(); // 获取配置

    // 商户私钥,api证书,整个文件放在cert文件夹下
    $merchantPrivateKeyFilePath = public_path() . 'cert/apiclient_key.pem';
    // 加载商户私钥
    $this->merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);

    // 商户证书
    $merchantCertificateFilePath = public_path() . 'cert/apiclient_cert.pem';
    // 加载商户证书
    $this->merchantCertificateInstance = PemUtil::loadCertificate($merchantCertificateFilePath);
    // 解析商户证书序列号
    $this->merchantCertificateSerial = PemUtil::parseCertificateSerialNo($this->merchantCertificateInstance);

    // 平台证书
    $platformCertificateFilePath = public_path() . 'cert/wechatpay_48E897EFB18EE05C97B4EEF20F64193Cxxxxxxxxx.pem';
    // 加载平台证书
    $this->platformCertificateInstance = PemUtil::loadCertificate($platformCertificateFilePath);
    // 解析平台证书序列号
    $this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateInstance);
}

public function wechatArousePay($order, $member)
{
    $PaymentAmount = bcmul($order['pay_price'], 100, 0);
    // 服务商商户号
    $merchantId = $this->config['service_member'];

    // 工厂方法构造一个实例
    $instance = Builder::factory([
        'mchid' => $merchantId,
        'serial' => $this->merchantCertificateSerial,
        'privateKey' => $this->merchantPrivateKeyInstance,
        'certs' => [
            $this->platformCertificateSerial => $this->platformCertificateInstance,
        ],
    ]);

    $params = [
        'sp_appid' => $this->config['wx_app_appid'],
        'sp_mchid' => $merchantId,
        'sub_mchid' => $order['sub_mchid'],
        'description' => '购买商品',
        'out_trade_no' => $order['out_trade_no'],
        'notify_url' => $this->config['wechat_notify'],
        'settle_info' => [
            'profit_sharing' => true,
        ],
        'amount' => [
            'total' => (int)$PaymentAmount,
            'currency' => 'CNY'
        ],
        'payer' => [
            'sp_openid' => $member['weapp_open_id'],
        ]
    ];

//    $authorization = $this->getAuthorization("POST", json_encode($params, JSON_UNESCAPED_UNICODE));
    try {
        $resp = $instance
            ->chain('v3/pay/partner/transactions/jsapi')
            ->post([
                'json' => $params,
              //  'headers' => [
              //      'Content-type' => 'application/json;charset=utf-8',
               //     'Accept' => 'application/json',
              //      'User-Agent' => '*/*',
               //     'Authorization' => $this->schema . " " . $authorization
              //  ],
            ]);

        $result_code = $resp->getStatusCode();
        if ($result_code == 200) {
            $result_data = json_decode($resp->getBody(), true);
            $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyInstance);
            $arouse_data = [
                'appId' => $this->config['wx_app_appid'],
                'timeStamp' => strval(Formatter::timestamp()),
                'nonceStr' => Formatter::nonce(),
                'package' => 'prepay_id=' . $result_data['prepay_id'],
            ];

            $arouse_data += ['paySign' => Rsa::sign(
                Formatter::joinedByLineFeed(...array_values($arouse_data)),
                $merchantPrivateKeyInstance
            ), 'signType' => 'RSA'];
            $arouse_data['out_trade_no'] = $order['out_trade_no'];
            return $this->setSuccess('success', $arouse_data);
        }
    } catch (\Exception $exception) {
        // 进行错误处理
        if ($exception instanceof RequestException && $exception->hasResponse()) {
            $r = $exception->getResponse();
            return $this->setError($r->getStatusCode(), $r->getReasonPhrase());
        } else {
            return $this->setError($exception->getMessage());
        }
    }
}


public function getAuthorization($http_method = "POST", $body = [], $url = "v3/pay/partner/transactions/jsapi"): string
{

    $serial_no = $this->merchantCertificateSerial; // 商户证书序列号
    $mchid = $this->config['service_member'];  // 商户号
    $timestamp = time();
    $nonce = $this->nonce_str();
    $url_parts = parse_url($url);

    $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
    $message = $http_method . "\n" .
        $canonical_url . "\n" .
        $timestamp . "\n" .
        $nonce . "\n" .
        $body . "\n";

    openssl_sign($message, $raw_sign, $this->merchantPrivateKeyInstance, 'sha256WithRSAEncryption');
    $sign = base64_encode($raw_sign);

    return sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
        $mchid, $nonce, $timestamp, $serial_no, $sign);
}

总结

"wechatpay-guzzle-middleware"这个sdk的用法有时间再更新。
后续分账的接口使用方法,下次继续更新。
“wechatpay-php”这个还是挺好用的。微信文档现在更新得可以了。
按照上面的写法,基本不会踩什么坑,我遇到问题的地方也记录下来供给参考了。
如果遇到什么新问题,也可以发出来我看看。

;