微信小程序商家转账到零钱V3 WeChatPay OpenAPI SDK

一、相关参考链接

1、微信小程序商家转账到零钱相关API文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml

2、wechatpay-php sdk 安装以及示例介绍:https://github.com/wechatpay-apiv3/wechatpay-php

3、证书相关参考链接(商户API证书、平台证书):https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_0.shtml

二、微信小程序商家转账到零钱开发示例(V3)

2.1、安装wechatpay-php sdk

composer require wechatpay/wechatpay

2.2、准备微信商家转账到零钱相关参数

  商户API证书:https://kf.qq.com/faq/161222NneAJf161222U7fARv.html

  商户API证书序列号:登录商户平台【API安全】->【API证书】->【查看证书】,可查看商户API证书序列号;也可通过解析相关商户API证书获取,参考证书相关链接

  商户号:可通过微信支付后台获取

  平台证书(明细转账金额 >= 2,000是需要此证书进行敏感信息加密):https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml

  APIv3秘钥(使用平台证书是才需要):https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_2.shtml

  小程序AppID和AppSecret:可通过微信公众平台登录相关公众号获取。【设置与开发】-> 【基本配置】

2.2、构建SDK客户端实例(详情参考wechatpay-php示例)

  注:1、需开启curl、openssl等相关扩展;2、可能会遇到请求报错的情况,请根据报错内容自行百度解决方案

  windows下报错:

  

 

  解决方案:下载相关证书 https://curl.se/docs/caextract.html 选个最新的即可

  在php.ini中搜索 curl.cainfo 取消注释,配置下载证书的相关路径,重新相关服务即可

  

  下面为构建实例以及转账和查询的参考代码,框架实例中使用的为thinkphp6。请还是仔细阅读官方文档

<?php
namespace app\api\WeChatPay;


use GuzzleHttp\Exception\RequestException;
use think\exception\HttpException;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;

class XcxPay{
    //证书路径 商户api证书和平台证书存放路径
    private static $certificatePath;
    //商户API证书公钥文件
    private static $merchantPublicFile = 'apiclient_cert.pem';
    //商户API证书私钥文件
    private static $merchantPrivateFile = 'apiclient_key.pem';
    //商户API证书序列号
    private static $merchantCertificateSerialNo;
    //商户号
    private static $merchantId;
    //商户平台APIv3秘钥
    private static $apiV3Key;
    //平台证书文件
    private static $platformFile = 'wx_public_cert.pem';
    //平台证书序列号
    private static $platformCertificateSerialNo;
    //小程序AppId
    private static $appId;
    //小程序Secret
    private static $appSecret;
    //APIv3 客户端实例
    private static $instance;
    //平台证书接口地址
    private static $platformCertificateUrl;
    //商户API私钥
    private static $merchantPrivateKeyInstance;

    /**
     * 初始化相关数据
     * XcxPay constructor.
     */
    public function __construct(){
        //>>证书存放路径
        self::$certificatePath = app_path().'api/WeChatPay/wx_cert/';
        //>>获取商户API证书序列号
        self::$merchantCertificateSerialNo = self::getMerchantCertificate(self::$certificatePath.self::$merchantPublicFile);
        //>>商户号
        self::$merchantId = 'xxxxxxxxxx';
        //>>小程序AppId
        self::$appId = 'xxxxxxxxxxxxxxxxx';
        //>>小程序秘钥(此处不会使用,获取小程序用户openid会用到,请参考官方文档即可)
        self::$appSecret = 'xxxxxxxxxxxxxxxxxxxxxx';
        //>>平台证书请求地址
        self::$platformCertificateUrl = 'https://api.mch.weixin.qq.com/v3/certificates';
        //>>apiv3秘钥,解密下载的平台证书时使用
        self::$apiV3Key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://'.self::$certificatePath.self::$merchantPrivateFile;
        self::$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        //>>判断是否存在平台证书,没有则生成
        if(!file_exists(self::$certificatePath.self::$platformFile)){
            try {
                self::downPlatformCertificate();
            }catch (\Exception $e){
                throw new HttpException('500','下载平台证书异常!');
            }
        }
        //>>构造一个 APIv3 客户端实例
        self::$instance = self::getInstance();
    }

    /**
     *小程序商户转账到零钱
     * @param $pay_data  -根据文档生成的转账数据
     * @param bool $is_sensitive_info -是否转账明细额度超过2000
     * @return \Psr\Http\Message\ResponseInterface|string|null
     */
    public function xcxTransferAccounts($pay_data,$is_sensitive_info = false){
        //>>示例数据为转账900000
        $tx_last_money = bcmul(900000,100);
        $pay_data = [
            'appid'=>self::$appId,
            'out_batch_no'=>'GS20220613094335CK9LW4804926900X',
            'batch_name'=>"用户提现",
            'batch_remark'=>'用户提现',
            'total_amount'=>(int)$tx_last_money,
            'total_num'=>1,
            'transfer_detail_list'=>[
                [
                    'out_detail_no'=>'GS20220613094335CK9LW4804926900X',
                    'transfer_amount'=>(int)$tx_last_money,
                    'transfer_remark'=>'提现',
                    'user_name'=>self::getEncrypt('小寒'),  //转账明细超过2000需要 详情查看文档
                    'openid'=>'oTiAE46JHDmW43b8_PffJo3hKDsA',
                ]
            ]
        ];
        //>>判断传入数据是否需要敏感数据加密
        $transfer_data = [
            'json' => $pay_data
        ];
        if($is_sensitive_info == true){
            $transfer_data = [
                'json' => $pay_data,
                'headers' => [
                    'Wechatpay-Serial' => self::$platformCertificateSerialNo, //平台证书序列号
                ],
            ];
        }
        //>>发送请求
        try {
            $resp = self::$instance
                ->chain('v3/transfer/batches')
                ->post($transfer_data);
            /*我这个位置就算转账成功也不会在这里输出结果*/
            //echo $resp->getStatusCode();
            //echo $resp->getBody();
            //>>返回对象数据
            return $resp;
        }catch (\Exception $e){
            /*进行转账错误处理 两种情况 1.交易请求的异常处理  2非交易状态的异常处理*/
            if ($e instanceof RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
                //返回对象数据
                return $resp;
                /*简单判断示例
                echo $resp->getStatusCode();
                echo $resp->getBody();
                if($resp->getStatusCode() == 200){
                    //>>交易成功 请详细阅读接口文档做出相应处理
                }else{
                    //>>交易失败
                }*/
            }else{
                //>>返回字符串数据
                return $e->getMessage();
            }
        }
    }

    /**
     * 商家明细单号查询明细单
     * @param $out_batch_no
     * @param $out_detail_no
     * @return \Psr\Http\Message\ResponseInterface|string|null
     */
    public function selectXcxTransferAccountsRe($out_batch_no,$out_detail_no){
        try {
            //>>其他接口可以参考此接口参数拼接
            $resp = self::$instance
                ->v3->transfer->batches->outBatchNo->_out_batch_no_->details->outDetailNo->_out_detail_no_
                ->get([
                    'out_batch_no'=>$out_batch_no,       //商家明细单号
                    'out_detail_no'=>$out_detail_no,     //商家批次单号
                ]);
            //>>返回对象数据
            return $resp;
        }catch (\Exception $e){
            if ($e instanceof RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
                return $resp;
            }else{
                return $e->getMessage();
            }
        }
    }

    /**
     * 敏感数据加密 明细金额超过2000时使用 详情参考文档
     * @param $str
     * @return string
     */
    private static function getEncrypt($str) {
        $public_key_path = "file://".self::$certificatePath.self::$platformFile;
        $public_key = file_get_contents($public_key_path);
        $encrypted = '';
        if (openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
            //base64编码
            $sign = base64_encode($encrypted);
        } else {
            throw new HttpException('500','敏感数据加密异常!');
        }
        return $sign;
    }
    /**
     * 构造一个 APIv3 客户端实例
     * @return \WeChatPay\BuilderChainable
     */
    private function getInstance(){
        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名(生成签名会使用到 所以放前面了)
        //$merchantPrivateKeyFilePath = 'file://'.self::$certificatePath.self::$merchantPrivateFile;
        //self::$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

        // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
        $platformCertificateFilePath =  'file://'.self::$certificatePath.self::$platformFile;
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        // 从「微信支付平台证书」中获取「证书序列号」
        self::$platformCertificateSerialNo = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
        // 构造一个 APIv3 客户端实例
        $instance = Builder::factory([
            'mchid'      => self::$merchantId,                  // 商户号
            'serial'     => self::$merchantCertificateSerialNo, // 「商户API证书」的「证书序列号」
            'privateKey' => self::$merchantPrivateKeyInstance,
            'certs'      => [
                self::$platformCertificateSerialNo => $platformPublicKeyInstance,
            ],
        ]);
        return $instance;
    }

    /**
     * 下载以及保存平台证书
     * @return false|string
     * @throws \SodiumException
     */
    private static function downPlatformCertificate(){
        $certificate = self::getPlatformCertificate();
        //>>判断是否存在相关证书数据
        if (!isset($certificate['data'])) {
            $error_msg = isset($certificate['message'])?$certificate['message']:'平台证书下载失败!';
            throw new HttpException('500',$error_msg);
        }
        $ciphertext = $certificate['data'][0]['encrypt_certificate']['ciphertext'];
        $associatedData = $certificate['data'][0]['encrypt_certificate']['associated_data'];
        $nonceStr = $certificate['data'][0]['encrypt_certificate']['nonce'];
        $data = self::decryptToString($ciphertext, $associatedData, $nonceStr);
        if (!$data) {
           throw new HttpException('500','获取证书解密失败');
        }
        //保存平台证书 (https://myssl.com/cert_decode.html)获取证书序列号
        file_put_contents(self::$certificatePath . 'wx_public_cert.pem', $data);
        return $data;
    }

    /**
     * 解密证书
     * @param $ciphertext
     * @param $associatedData
     * @param $nonceStr
     * @return false|string
     * @throws \SodiumException
     */
    private static function decryptToString($ciphertext, $associatedData, $nonceStr){
        $str = base64_decode($ciphertext);
        if (strlen($str) <= 16) {
            return '';
        }
        // ext-sodium (default installed on >= PHP 7.2) 如果没有该函数需要安装sodium扩展
        return sodium_crypto_aead_aes256gcm_decrypt($str, $associatedData, $nonceStr, self::$apiV3Key);
    }
    /**
     * 获取平台证书列表
     * @return mixed
     */
    private static function getPlatformCertificate(){
        //当前时间戳
        $timestamp = time();
        //随机字符串,无要求符合微信生成随机数算法即可
        $nonce_str = self::get32Str();
        //报文主体,此接口为空
        $body = '';
        //>>生成平台证书请求签名的数据
        $sign_param = [
            'http_method'=>'GET',
            'timestamp'=>$timestamp,
            'body'=>$body,
            'nonce_str'=>$nonce_str,
        ];
        $sign = self::sign($sign_param);
        $header = [
            'Authorization:'.$sign,
            'Accept:application/json',
            'User-Agent:'.self::$merchantId,
        ];
        $result = self::wxCurl(self::$platformCertificateUrl,$header, '', 'GET');
        $result = json_decode($result, true);
        return $result;
    }

    /**
     * 生成签名
     * @param $sign_param
     * @return string
     */
    private static function sign($sign_param){
        //解析url
        $url_parts = parse_url(self::$platformCertificateUrl);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $message = $sign_param['http_method']."\n".
            $canonical_url."\n".
            $sign_param['timestamp']."\n".
            $sign_param['nonce_str']."\n".
            $sign_param['body']."\n";
        openssl_sign($message, $raw_sign, self::$merchantPrivateKeyInstance, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);
        $schema = 'WECHATPAY2-SHA256-RSA2048 ';
        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            self::$merchantId, $sign_param['nonce_str'], $sign_param['timestamp'], self::$merchantCertificateSerialNo, $sign);
        return $schema.$token;
    }

    /**
     *发送
     * @param $url
     * @param $header
     * @param array $data
     * @param string $method
     * @param int $time_out
     * @return bool|string
     */
    private static function wxCurl($url, $header, $data = [], $method = 'POST', $time_out = 3){
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_TIMEOUT, $time_out);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        if ($method == 'POST') {
            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        // 执行操作
        $result = curl_exec($curl);
        curl_close($curl);
        return $result;
    }

    /**
     * 根据商户api证书获取商户api证书序列号
     * @param $filepath
     * @return mixed
     */
    private static function getMerchantCertificate($filepath) {
        try {
            $resource = openssl_x509_read(file_get_contents($filepath));
            $resource_arr = openssl_x509_parse($resource);
            return strtolower($resource_arr['serialNumberHex']);
        }catch (\Exception $e){
            throw new HttpException('500','解析商户API证书序列号异常!');
        }
    }

    /**
     * 生成随机18位字符串加上当前日期14位字符串 共32位随机字符串数据
     * @return string
     */
    private static function get32Str(){
        $str = 'ABCDEFGHIJKLMNOPQLSTUVWXYZ0123456789';
        return mb_substr(str_shuffle($str),0,18).date('YmdHis',time());
    }
}

 

 

 

 

  

 

 

 

 

 

 

 

  

 

posted @ 2022-06-16 15:49  小寒、  阅读(1824)  评论(0)    收藏  举报