PHP对接第三方支付渠道之微信支付v3版本

文接上篇PHP如何更科学地接入第三方渠道,既然已经写到这了,索性创建了一个gitee仓库,地址:https://gitee.com/wuzhh/tp6-payment,有需要的可以去看看。

言归正传,微信支付v3版本刚推出不久,鉴于微信官方一贯语焉不详的尿性,论坛上自然仍旧一片哀嚎,鄙人一路踩坑下来,倒也还算顺利,把过程分享给大家参考~

一、微信支付平台相关配置

1. 配置API证书和API v3密钥

这一步按照官方提示步骤来即可,比较简单不再赘述

2. 分别加载guzzlehttp和wechatpay的composer包

composer require guzzlehttp/guzzle:~6.3
composer require wechatpay/wechatpay-guzzle-middleware:^0.2.2

3. 生成微信支付平台证书

此处千万注意,第1步中导出的证书有三个文件,以我的经验我只用到了apiclient_key.pem,而apiclient_cert.pem则没什么用(我目前没发现它有什么用),我们拿到apiclient_key.pem的路径之后:

  • cd进入项目根目录,接着cd到vendor/wechatpay/wechatpay-guzzle-middleware下
  • 执行如下shell命令:
php tool/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath} -c ${wechatpayCertificateFilePath}

上面是官方提供的命令,此处mchPrivateKeyFilePath为apiclient_key.pem的路径,outputFilePath为微信支付平台证书的存储路径,到这如果看文档不详细的可能会卡一下,由于是第一次创建,-c参数根本不知道,没办法验签,似乎陷入了死胡同?微信官方其实是考虑到了这一点的,所以第一次提交的时候 -c不需要填写。。。

4. 将微信支付的配置填入payment表

CREATE TABLE `payment`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '渠道ID',
  `channel_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道名称',
  `channel_code` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道代码',
  `channel_logo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道LOGO',
  `slogan` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '支付渠道标语',
  `class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '处理模块',
  `merchant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商户ID',
  `appid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Appid',
  `app_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'App Secret',
  `gateway_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付地址',
  `notify_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '通知地址',
  `cipher_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RSA' COMMENT '加密方式',
  `private_key` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '加密私钥,通常rsa模式需要',
  `public_key` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '加密公钥,通常rsa模式需要',
  `api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '加密秘钥,通常md5模式需要',
  `extra_params` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外参数',
  `format` char(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'JSON' COMMENT '接口参数格式,默认json',
  `return` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'success' COMMENT '回调成功返回标识,success',
  `os` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支持系统,android,ios',
  `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态,1=开启,0=关闭',
  `sort` int(10) NULL DEFAULT NULL COMMENT '排序值',
  `pay_test` tinyint(1) NOT NULL DEFAULT 0 COMMENT '测试模式,开启后支付1分',
  `created_at` int(10) NULL DEFAULT NULL COMMENT '创建时间',
  `updated_at` int(10) NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_code`(`channel_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

二、微信支付后台处理模块

1. 为了确保所有支付类有共同的调用接口,实现支付接口类

<?php

namespace payment;

interface PaymentInterface {
    public function doPay($params);
    public function notify($content);
}

这样就能确保上文注入的支付实例都有共同的支付和回调方法

2. 微信支付具体逻辑:


<?php

namespace payment\wxpay;

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException;
use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
use WechatPay\GuzzleMiddleware\Util\PemUtil;

use app\model\Payment;
use payment\PaymentInterface;

/**
 * 微信支付
 */
class WxPay implements PaymentInterface
{
    private $appid;                 // appid
    private $appSecret;             // appSecret
    private $apiV3Key;              // V3秘钥
    private $notifyUrl;             // 通知地址
    private $gatewayUrl;            // 接口地址
    private $merchantId;            // 商户号
    private $merchantSerialNumber;  // 商户API证书序列号
    private $merchantPrivateKey;    // 商户私钥
    private $payCertificate;        // 支付平台证书
    private $privateKey;            // 商户私钥
    private $publicKey;             // 商户公钥
    private $payTest;               // 测试支付开关
    private $extraConfigs;          // 额外配置

    public function __construct()
    {
        // code...
    }

    public function config(Payment $config)
    {
        $this->appid                = $config->appid;
        $this->appSecret            = $config->app_secret;
        $this->extraConfigs         = $config->extra_params;
        $this->apiV3Key             = $this->extraConfigs->apiV3Key ?? '';
        $this->notifyUrl            = $config->notify_url;
        // https://api.mch.weixin.qq.com/pay/unifiedorder
        $this->gatewayUrl           = $config->gateway_url;                                             // 支付网关
        // 商户相关配置
        $this->merchantId           = $config->merchant_id;                                             // 商户号
        $this->merchantSerialNumber = $this->extraConfigs->serialNumber ?? '';                          // 商户API证书序列号
        $this->merchantPrivateKey   = PemUtil::loadPrivateKeyFromString($config->private_key);          // 商户私钥
        // $this->merchantPrivateKey    = PemUtil::loadPrivateKey(dirname(__FILE__)."/apiclient_key.pem");          // 商户私钥
        $this->privateKey           = $config->private_key;                                             // 商户私钥
        // 微信支付平台配置
        $this->payCertificate       = PemUtil::loadCertificateFromString($config->public_key);          // 微信支付平台证书
        // $this->payCertificate        = PemUtil::loadCertificate(dirname(__FILE__)."/wechatpay.pem");             // 微信支付平台证书
        $this->publicKey            = $config->public_key;                                              // 微信支付平台证书
        // 测试支付开关
        $this->payTest              = $config->pay_test;
    }

    /*
    * 支付方法
    */
    public function doPay($params)
    {
        if (! $this->apiV3Key) {
            return ['status' => 0, 'message' => '支付API秘钥未配置'];
        }
        if (! $this->merchantSerialNumber) {
            return ['status' => 0, 'message' => '商户API证书序列号未配置'];
        }

        // 构造一个WechatPayMiddleware
        $wechatpayMiddleware = WechatPayMiddleware::builder()
            ->withMerchant($this->merchantId, $this->merchantSerialNumber, $this->merchantPrivateKey) // 传入商户相关配置
            ->withWechatPay([ $this->payCertificate ])  // 可传入多个微信支付平台证书,参数类型为array
            ->build();

        // 将WechatPayMiddleware添加到Guzzle的HandlerStack中
        $stack = HandlerStack::create();
        $stack->push($wechatpayMiddleware, 'wechatpay');

        // 创建Guzzle HTTP Client时,将HandlerStack传入
        $client = new Client(['handler' => $stack]);

        // 接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
        try {
            $trade_type = $params['trade_type'] ?? 'app';

            $requestData = [
                'json' => [ // JSON请求体
                    'appid'         => $this->appid,
                    'mchid'         => $this->merchantId,
                    'description'   => $params['goods_name'],
                    'out_trade_no'  => $params['order_no'],
                    'notify_url'    => $this->notifyUrl,
                    'amount'        => [
                        'total'             => !$this->payTest ? $params['amount'] : 1,
                        'currency'          => 'CNY'
                    ],
                    'scene_info'    => [
                        'payer_client_ip'   => $params['client_ip'] ?? '127.0.0.1'
                    ],
                    'attach'        => $params['attach'] ?? ''
                ],
                'headers' => ['Accept' => 'application/json']
            ];

            if ($trade_type == 'jsapi') {

                if (!isset($params['openid']) || empty($params['openid'])) {
                    return ['status' => 0, 'message' => 'Openid不能为空'];
                }

                $requestData['json']['payer'] = [
                    'openid' => $params['openid']
                ];
            }

            if ($trade_type == 'h5') {
                $requestData['json']['scene_info']['h5_info']['type'] = "Wap";
            }

            $resp = $client->request('POST', $this->gatewayUrl . "pay/transactions/{$trade_type}", $requestData);

            $content = $resp->getBody();
            $data = json_decode($content, true);

            $ret = [];
            switch ($trade_type) {
                // APP支付
                case 'app':
                    $ret = ['orderInfo' => $this->getOrderInfo($data['prepay_id'])];
                    break;
                // 公众号支付
                case 'jsapi':
                    $ret = ['prepay_id' => $data['prepay_id']];
                    break;
                // h5支付
                case 'h5':
                    $ret = ['h5_url' => $data['h5_url']];
                    break;
                // 扫码支付
                case 'native':
                    $ret = ['code_url' => $data['code_url']];
                    break;
                
                default:
                    # code...
                    break;
            }
        } catch (RequestException $e) {
            // 进行错误处理
            $data = [];
            if ($e->hasResponse()) {
                // echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
                // echo $e->getResponse()->getBody();
                $content = $e->getResponse()->getBody();
                $data  = json_decode($content, true);
            }

            return ['status' => 0, 'message' => $data['message'] ?? '支付失败:'.$e->getMessage()];
        }

        return ['status' => 1, 'message' => 'SUCCESS', 'data' => $ret];
    }

    public function getOrderInfo($prepay_id)
    {
        $nonceStr   = randomString(16);
        // $package     = "prepay_id={$prepay_id}";
        $package    = "Sign=WXPay";
        $timestamp  = time();
        $paySign    = $this->paySign($this->appid, $timestamp, $nonceStr, $package);
        return [
            'appid'     => $this->appid,
            'noncestr'  => $nonceStr,
            'package'   => $package,
            'partnerid' => $this->merchantId,
            'prepayid'  => $prepay_id,
            'timestamp' => $timestamp,
            'sign'      => $paySign,
        ];
    }

    public function transactions(array $params) 
    {
        $url = '';
        if (isset($params['transaction_id']) && !empty($params['transaction_id'])) {
            $url = 'pay/  transactions/id/'.$params['transaction_id'];
        }
        if (isset($params['out_trade_no']) && !empty($params['out_trade_no'])) {
            $url = 'pay/transactions/out-trade-no/'.$params['out_trade_no'];
        }

        if (empty($url)) {
            return false;
        }

        $url .= '?mchid='.$this->merchantId;

        // 构造一个WechatPayMiddleware
        $wechatpayMiddleware = WechatPayMiddleware::builder()
            ->withMerchant($this->merchantId, $this->merchantSerialNumber, $this->merchantPrivateKey) // 传入商户相关配置
            ->withWechatPay([ $this->payCertificate ])  // 可传入多个微信支付平台证书,参数类型为array
            ->build();

        // 将WechatPayMiddleware添加到Guzzle的HandlerStack中
        $stack = HandlerStack::create();
        $stack->push($wechatpayMiddleware, 'wechatpay');

        // 创建Guzzle HTTP Client时,将HandlerStack传入
        $client = new Client(['handler' => $stack]);

        // 接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
        try {
            $resp = $client->request('GET', $this->requestUrl . $url,[
                'headers' => ['Accept' => 'application/json']
            ]);

            // echo $resp->getStatusCode().' '.$resp->getReasonPhrase()."\n";
            // echo $resp->getBody()."\n";exit;
            $content = $resp->getBody();
            $data = json_decode($content, true);

            if ($data['trade_state'] === 'SUCCESS') {
                return ['status' => 1, 'message' => "SUCCESS", 'data' => $data];
            }else{
                return ['status' => 0, 'message' => $data['trade_state_desc'] ?? '', 'trade_state' => $data['trade_state']];
            }
        } catch (RequestException $e) {
            // 进行错误处理
            $res_data = [];
            if ($e->hasResponse()) {
                $content = $e->getResponse()->getBody();
                $res_data = json_decode($content, true);
            }

            return ['status' => 0, 'message' => $res_data['message'] ?? '支付失败'];
        }
    }

    public function decryptCiphertext($ciphertext)
    {
        $ciphertext = $this->urlsafe_b64decode($ciphertext);
        $privateKey = $this->privateKey;
        $iv = substr($privateKey, 0, 16);

        $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $privateKey, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
        return $decrypted;
    }

    // 支付签名
    public function paySign($appId, $timeStamp, $nonceStr, $package)
    {
        $str = "{$appId}\n{$timeStamp}\n{$nonceStr}\n{$package}\n";
        $privateKey = $this->privateKey;
        openssl_sign($str, $encrypt_data, openssl_get_privatekey($privateKey), 'sha256WithRSAEncryption');
        $encrypt_data = base64_encode($encrypt_data);
        return $encrypt_data;
    }

    // 解密数据
    public function decryptSign($ciphertext, $associatedData, $nonceStr)
    {
        $ciphertext = base64_decode($ciphertext);

        if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
            //$APIv3_KEY就是在商户平台后端设置是APIv3秘钥
            $orderData = \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->apiV3Key);
            $orderData = json_decode($orderData, true);

            return $orderData;
        }else{
            exit('缺乏PHP扩展:sodium,请安装该扩展或切换到PHP7.3+版本');
        }
        
        return $result;
    }

    /**将字符串安全编码
     *
     * @param  $string
     *
     * @return string
     */
    public function urlsafe_b64encode($string) 
    {
        $data = base64_encode($string);
        $data = str_replace(array('+', '/', '='), array('-', '_', ''), $data);
        return $data;
    }

    /**将字符串安全解码
     *
     * @param  $string
     *
     * @return string
     */
    public function urlsafe_b64decode($string) 
    {
        $data = str_replace(array('-', '_'), array('+', '/'), $string);
        $mod4 = strlen($data) % 4;
        if ($mod4) {
            $data .= substr('====', $mod4);
        }
        return base64_decode($data);
    }

    public function notify($content)
    {
        // 通知部分过段时间再更,端午节就更到这了
    }
}

有部分方法文中没上,我个人的项目中用上了,就暂且保留吧

3. 微信支付回调逻辑:

通知部分过段时间再更,明天就算端午节就更到这了,有需要的小伙伴可以留言,我争取尽快补上~

推荐阅读更多精彩内容