微信v3支付【php】

微信v3支付

    • 加载guzzlehttp和wechatpay的composer包
    • 案例

目前只使用到jsapi支付。其他支付尚未尝试,如有bug请自己调试。

加载guzzlehttp和wechatpay的composer包

在composer.json 添加包信息 然后执行composer update

"guzzlehttp/guzzle": "^7.4",
"guzzlehttp/uri-template": "^1.0",
"wechatpay/wechatpay": "^1.4"

composter手册

案例


/* *
 * 功能:微信v3支付
 * 修改日期:2021-12-21
 */
namespace plugins\wxapp\service;


use think\Cache;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
use think\DB;


class WxpayService
{

    public $merchantId = '';
    public $merchantPrivateKeyFilePath = '';
    public $merchantPrivateKeyInstance = '';
    public $merchantCertificateSerial = '';
    public $platformCertificateFilePath = '';
    public $platformPublicKeyInstance = '';
    public $platformCertificateSerial = '';
    public $appid = '';
    public $secret = '';
    public $paySecret = '';
    public $v3Key = '';
    public $payTest = 0;

    public $instance;


    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;


    function __construct()
    {

        $config = $this->getConfig();

//        $this->appid = 'wxd03860b2b3xxx';
        $this->appid = $config['appid'];

        // 商户号
//        $this->merchantId = '161641xxx';
        $this->merchantId = $config['merchantId'];

        // 从本地文件中加载商户API私钥,商户API私钥会用来生成请求的签名
//        $this->merchantPrivateKeyFilePath = 'file://'.ROOT_PATH.'public/apiclient_key.pem';
        $this->merchantPrivateKeyFilePath = trim($config['merchantPrivateKeyFilePath']); // 可以直接放密钥内容

        $Rsa = new Rsa();
        $this->merchantPrivateKeyInstance =  $Rsa::from($this->merchantPrivateKeyFilePath, $Rsa::KEY_TYPE_PRIVATE);

        // 商户API证书序列号
//        $this->merchantCertificateSerial = '542A7C2F14E8F87B52DB3Fxxxxxxx';
        $this->merchantCertificateSerial = $config['merchantCertificateSerial'];

        // 公众号secret
//        $this->secret = '6498b41d0ee84cf7749xxxxxx';
        $this->secret = $config['secret'];

        // 商户secret
//        $this->paySecret = '825923d02c1649fef61xxxxxx';
        $this->paySecret = $config['paySecret'];

        // v3密钥
//        $this->v3Key = 'wVrkTp8HCYWg6OZ0Ic5Pzfxxxxx';
        $this->v3Key = $config['v3Key'];

        // 是否测试支付
//        $this->payTest = 1;
        $this->payTest = $config['payTest'];
//        dump($config);
        // 使用 bin/CertificateDownloader.php 生成的参数,并非生成私钥时生成的 apiclient_cert.pem
        // 命令 php ./CertificateDownloader.php -k=wVrkTp8HCYWg6OZ0xxxzxxx -m=16164xxxx -f=/mnt/www/wwwroot/path_com/Zerg/public/apiclient_key.pem -s=542A7C2F14E8F87B52DB3F6Axxx -o=/mnt/www/wwwroot/path_com/Zerg/public/wx_cert.pem
        // 对应参数请查看 https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md
        // 从本地文件中加载微信支付平台证书,用来验证微信支付应答的签名
//        $this->platformCertificateFilePath = 'file://'.ROOT_PATH.'public/apiclient_cert.pem';
        $this->platformCertificateFilePath = trim($config['platformCertificateFilePath']);// 可以直接放密钥内容

        $this->platformPublicKeyInstance = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        // 获取微信支付平台证书序列号
        $this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateFilePath);
        // 构造一个 APIv3 客户端实例
        $this->instance = Builder::factory([
            'mchid'      =>  $this->merchantId,
            'serial'     =>  $this->merchantCertificateSerial,
            'privateKey' =>  $this->merchantPrivateKeyInstance,
            'certs'      => [
                $this->platformCertificateSerial =>  $this->platformPublicKeyInstance,
            ],
        ]);
    }

    /**
     * 统一下单
     * @param $params
     * WIDtotal_amount 金额
     * WIDsubject 描述
     * WIDout_trade_no 订单号
     * openid 用户openid(trade_type=jsapi 需要 openid)
     * trade_type 支付方式
     * notifyUrl 回调地址 
     * @return false
     */
    public function doPay($params)
    {
        // 手册
        // https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_6_3.shtml
        // https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_1.shtml
        //注意: 提醒前端请查看 v3的手册 。签名方式不一样

        $trade_type = $params['trade_type'] ?? 'h5';

        $amount = (int)(bcmul("{$params['WIDtotal_amount']}","100")); //元转分
//        dump($amount);
//        dump($this->payTest);
        $requestData = [
            'json' => [ // JSON请求体
                'appid'         => $this->appid,
                'mchid'         => $this->merchantId,
                'description'   => $params['WIDsubject'],
                'out_trade_no'  => $params['WIDout_trade_no'],
                'notify_url'    => $params['notifyUrl'], // 回调地址
                'amount'        => [
                    'total'             => !$this->payTest ? $amount : 1,
                    'currency'          => 'CNY'
                ],
                'scene_info'    => [
                    'payer_client_ip'   => get_client_ip()
                ],
                'attach'        => $params['attach'] ?? '' //附加数据
            ],
            'headers' => ['Accept' => 'application/json']
        ];
//        dump($requestData);exit;

        if ($trade_type == 'jsapi') {

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

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

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

        try{
            $resp = $this->instance
                ->chain("v3/pay/transactions/{$trade_type}")
                ->post($requestData);
//            echo $resp->getStatusCode(), PHP_EOL;
//            echo $resp->getBody(), PHP_EOL;
            $content = $resp->getBody();
            $data = json_decode($content, true);

            $ret = $this->getTypeInfo($trade_type,$data);

            $info['code'] =  $resp->getStatusCode();
            $info['data'] = $ret;
//            echo $resp->getBody(), PHP_EOL;

            return $info;

        } catch (\Exception $e) {

            // 进行错误处理
//            echo $e->getMessage(), PHP_EOL;
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $code = $r->getStatusCode();
//                echo $r->getReasonPhrase(), PHP_EOL;
//                echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
                $data = json_decode($r->getBody(),true);
                if(isset($data['prepay_id'])){
                    $info['code'] =  200;
                    $ret = $this->getTypeInfo($trade_type,$data);
                    $info['data'] = $ret;
                    return $info;
                }
//                echo $r->getReasonPhrase(), PHP_EOL;
//                echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
            }

//            echo $e->getTraceAsString(), PHP_EOL;
            return false;
        }
    }

    /**
     * 获得各支付类型的返回值
     * @param $trade_type
     * @param $data
     * @return array|array[]
     */
    public function getTypeInfo($trade_type,$data){
        switch ($trade_type) {
            // APP支付
            case 'app':
                $ret = ['orderInfo' => $this->getOrderInfo($data['prepay_id'])];
                break;
            // 公众号支付
            case 'jsapi':
                $ret = ['jsApiInfo' => $this->getJsApiInfo($data['prepay_id'])];
                break;
            // h5支付
            case 'h5':
                $ret = ['h5_url' => $data['h5_url']];
                break;
            // 扫码支付
            case 'native':
                $ret = ['code_url' => $data['code_url']];
                break;

            default:

                break;
        }
        return $ret;

    }

    /**
     * jsapi签名
     * @param $prepay_id
     * @return array
     */
    public function getJsApiInfo($prepay_id){
        $info =  [
            "appId"=>$this->appid,     //公众号名称,由商户传入
            "timeStamp"=>time(),         //时间戳,自1970年以来的秒数
            "nonceStr"=>$this->getNonceStr(16), //随机串
            "package"=>"prepay_id=$prepay_id",
        ];
        $paySign = $this->RSAsign($info,$this->merchantPrivateKeyFilePath);

        return $paySign;
    }

    /**
     * 微信md5签名
     * @param $para mixed 带签名参数数组
     * @param $wx_key string wxkey
     */
    public static function MD5sign($array, $wx_key) {
        ksort($array);
        $stringA = urldecode(http_build_query($array));
        $stringSignTemp="$stringA&key=".$wx_key;
        return strtoupper(md5($stringSignTemp));
    }

    /**
     * 微信rsa签名
     * @param $params
     * @param $merchantPrivateKeyInstance
     * @return array
     */
    public static function RSAsign($params,$merchantPrivateKeyInstance) {

        $params += ['paySign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'signType' => 'RSA'];

        return $params;
    }

    public function getOrderInfo($prepay_id)
    {
        $nonceStr   = $this->getNonceStr(16);
        $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,
        ];
    }

    /**
     * 随机字符串
     * @param int $length
     * @return string
     */
    public static function getNonceStr($length = 16)
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str ="";
        for ( $i = 0; $i < $length; $i++ )  {
            $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }



    /**
     * 获取AccessToken
     * @param $code
     * @return mixed|string
     */
    public function getAccessToken($code,$user_id){

        $accessToken = Cache::get('accessToken:'.$user_id);
        if(!isset($accessToken['errcode'])){
            if($accessToken) return $accessToken;
        }

        $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$this->appid&secret=$this->secret&code=$code&grant_type=authorization_code";
        $ch = curl_init();
        //3.设置参数
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        //4.调用接口
        $res = curl_exec($ch);
//        if(curl_errno($ch)){
//            var_dump(curl_error($ch));
//        }
        //5.关闭curl
        curl_close($ch);

        $arr = json_decode($res, true);
        if(isset($arr['errcode'])){
//            var_dump($arr);
            return false;
        }
        Cache::set('accessToken:'.$user_id,$arr,'7200');

        return $arr;
    }

    // 回调

    /**
     * 回调解码
     * @param $post_data
     * @return mixed|void
     */
    public function notify($post_data)
    {
//        https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_5.shtml 手册

//        Db::name('test_log')->insert(['create_time'=>time(),'name'=>'微信支付v3回调','log'=>json_encode($content,256)]);
//        $str = '{"id":"8a8eb699-67c0-542e-933e-4dfb36f0919f","create_time":"2021-12-21T14:34:26+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"WAtQqUDtf\/QvlqLwxg0kqAx4j4YKFlSiMEgAQvXe87Iarl4LT\/SebdO\/2vCFBK7LNWoEiW6opFkk7Wl+lhlKLYPR+7xIgFVFDqqkNPemtJMeJH+yF5BFCiiGOATDJTThXh81qFqUk0GT3FHnzjlmUN4+t3jlTtr9Rrlm5YZ5lsvv4IAiOJJR5JrUABbapxvykFHAtVWi004\/wuTuSHYySRCB7mbCZybjnOrzKJxi0p1zZ553FKxk83jwk14xA\/ijBRqr\/9hbFSfxUm9xna3BfCocg1dFsRgTUvfmIEvVG7J1se8StD5O820wjtZCri5aE3rTqumIMmgTqKvUtCqfZ5dahzoSe0iR6nBDNNP01Tb0PH2QyriIomivlvMDoe3PI93ZsPD3vgO8Tmb6LxbHArkr0Xnn4fkXaJwTwrPDb2IcN8dr3c82AiuZ6u8M8x1c+JOl83bDcVtE3rPGVMwgyqg\/Ee8D5mFK8DgZ4ohzf2WWX7+itaLSLZPY\/VAlzt0T47Xc1QC6giF+qRnn0DAYVTxkd9F93QdLSrt9NArZlx+01Cp5sGeMsH8tdwyC\/P7VNdw=","associated_data":"transaction","nonce":"TQ68zkQ9LvpD"},"_plugin":"wxapp","_controller":"notify","_action":"notify"}';
//        $post_data = json_decode($str,true);

        if ($post_data['event_type'] == 'TRANSACTION.SUCCESS') {
            $nonceStr = $post_data['resource']['nonce'];
            $associatedData = $post_data['resource']['associated_data'];
            $ciphertext = $post_data['resource']['ciphertext'];
            $ciphertext = base64_decode($ciphertext);
            if (strlen($ciphertext) <= 12) {
                return;
            }
            $result = $this->wechartDecrypt($ciphertext,$associatedData,$nonceStr);
            return $result;
//            array(12) {
//                ["mchid"] => string(10) "1616410845"
//                ["appid"] => string(18) "wxd03860xxxx"
//                ["out_trade_no"] => string(19) "202112211xxxx"
//                ["transaction_id"] => string(28) "42000013052xxxx27428"
//                ["trade_type"] => string(5) "JSAPI"
//                ["trade_state"] => string(7) "SUCCESS"
//                ["trade_state_desc"] => string(12) "支付成功"
//                ["bank_type"] => string(6) "OTHERS"
//                ["attach"] => string(0) ""
//                ["success_time"] => string(25) "2021-12-21T14:34:26+08:00"
//                ["payer"] => array(1) {
//                ["openid"] => string(28) "ohH0s5g9GVDZHrPFTRpipxxxx"
//                                      }
//                ["amount"] => array(4) {
//                    ["total"] => int(1)
//                    ["payer_total"] => int(1)
//                    ["currency"] => string(3) "CNY"
//                    ["payer_currency"] => string(3) "CNY"
//                                      }
//             }

        }
        return ;
    }


    /**
     * 微信回调解密
     * @param $ciphertext
     * @param $associatedData
     * @param $nonceStr
     * @return mixed|string
     */
    public function wechartDecrypt($ciphertext,$associatedData, $nonceStr) {

        $key = $this->v3Key;
        // ext-sodium (default installed on >= PHP 7.2)
        if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
            $str = \ sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $key);
            return json_decode($str,true);
        }

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\ crypto_aead_aes256gcm_is_available()) {
            $str = \ Sodium\ crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $key);
            return json_decode($str,true);
        }

        // openssl (PHP >= 7.1 support AEAD)
        if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            $str = \ openssl_decrypt($ctext, 'aes-256-gcm', $key, \OPENSSL_RAW_DATA, $nonceStr,
                $authTag, $associatedData);
            return json_decode($str,true);
        }

        return '';
        throw new\ RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }

    /**
     * 获取配置
     * @return mixed
     */
    public function getConfig(){
        $payconfig = DB::name('plugin')->where('id',8)->value('config');
        return json_decode($payconfig,true);
    }


}
?>

你可能感兴趣的:(php,微信,php)