美文网首页
微信小程序JSAPI下单 APIv3 PHP

微信小程序JSAPI下单 APIv3 PHP

作者: 码农工号9527 | 来源:发表于2022-03-04 20:17 被阅读0次

    JSAPI下单

    商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。

    文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml

    接口说明
    适用对象: 直连商户

    请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

    请求方式:POST

    public function jsApiPlaceOrder()
    {
        $params = \request()->all();//请求参数
        /**
        * 应用ID          appid
        * 直连商户号       mchid
        * 商品描述         description
        * 商户订单号       out_trade_no
        * 交易结束时间     time_expire
        * 附加数据         attach
        * 通知地址         notify_url
        * 总金额           total
        * 货币类型         currency  CNY:人民币,境内商户号仅支持人民币。
        * 用户标识         openid
        */
        $data_arr['mchid'] = '直连商户号';
        $data_arr['out_trade_no'] = '商户订单号';
        $data_arr['appid'] = '应用ID';
        $data_arr['description'] = '商品描述';
        $data_arr['notify_url'] = '通知地址';
        $data_arr['attach'] = '附加数据';
        $data_arr['amount'] = [
            'total' => '总金额',
            'currency' => '货币类型',
        ];
        $data_arr['payer'] = [
            // 通过前端传入的jscode换取openid,$get_config 包含了appid,密钥等信息的数组
            'openid' => $this->GetOpenidFromMp($params['code'], $get_config) // 用户标识
        ];
        
        $url_parts = parse_url('https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi');
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $timestamp = time();
        $nonce = $this->getNonceStr();
        $body = json_encode($data_arr);
        $schema = 'WECHATPAY2-SHA256-RSA2048';
        $sign = $this->getWxRSA256Sign([
            'POST'
            , $canonical_url
            , $timestamp
            , $nonce
            , $body
        ], '商户密钥');
    
        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            '直连商户号', $nonce, $timestamp, '69B99084AF6C21B0D57C152BFE804851327CAAF3', $sign);
    
        $re = $this->httpRequest(
            'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
            , 'POST'
            , $body
            , [
                'Authorization:' . $schema . ' ' . $token
                , 'Accept: application/json'
                , 'Content-Type: application/json'
                , 'User-Agent: '.\request()->header('user-agent', '')
            ]
        );
    
        $re = json_decode($re,true);
        if(!empty($re) && isset($re['prepay_id'])) {
    
            $args['timeStamp'] = (string) time();
            $args['nonceStr'] = $this->getNonceStr();
            $args['package'] = 'prepay_id='.$re['prepay_id'];
            $args['signType'] = 'RSA';
            $args['paySign'] = $this->getWxRSA256Sign([
                '应用ID'
                , $args['timeStamp']
                , $args['nonceStr']
                , $args['package']
            ], '商户密钥');
    
            if($args['paySign']) {
                // 将生成的参数返回给前端,给它调用wx.requestPayment(OBJECT)发起微信支付
                return ['code' => 0, 'msg' => 'success', 'data' => $args];
            } else {
                Log::channel(\request('game'))->info(__CLASS__.';'.__FUNCTION__.";小程序调起支付的签名计算异常", [$args, $get_config]);
                return ['code' => 1, 'msg' => '支付签名计算异常'];
            }
    
        } else {
            Log::channel(\request('game'))->info(__CLASS__.';'.__FUNCTION__.";prepay_id 结果异常", [$params, $re]);
            return ['code' => 1, 'msg' => '调起支付异常'];
        }
    }
    
    /**
     * 小程序调起支付的签名计算
     * @param array $sign_arr
     * @param string $apiclient_key
     * @return string
     */
    private function getWxRSA256Sign(array $sign_arr, string $apiclient_key) :string
    {
    
        $message = '';
        foreach ($sign_arr as $value) $message .= $value."\n";
    
        $pri_key = "-----BEGIN RSA PRIVATE KEY-----\n" .
            wordwrap($apiclient_key, 64, "\n", true) .
            "\n-----END RSA PRIVATE KEY-----";
        $pi_key =openssl_pkey_get_private($pri_key);
        openssl_sign($message, $raw_sign, $pi_key, 'sha256WithRSAEncryption');
    
        return base64_encode($raw_sign);
    }
    
    private function httpRequest($url,$method,$param,$headers,$post_file=false)
    {
        $oCurl = curl_init();
        if(stripos($url,"https://")!==FALSE){
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); //CURL_SSLVERSION_TLSv1
        }
    
        if (PHP_VERSION_ID >= 50500 && class_exists('\CURLFile')) {
            $is_curlFile = true;
        } else {
            $is_curlFile = false;
            if (defined('CURLOPT_SAFE_UPLOAD')) {
                curl_setopt($oCurl, CURLOPT_SAFE_UPLOAD, false);
            }
        }
    
        if (is_string($param)) {
            $strPOST = $param;
        } elseif($post_file) {
            if($is_curlFile) {
                foreach ($param as $key => $val) {
                    if (substr($val, 0, 1) == '@') {
                        $param[$key] = new \CURLFile(realpath(substr($val,1)));
                    }
                }
            }
            $strPOST = $param;
        } else {
            $aPOST = array();
            foreach($param as $key=>$val){
                $aPOST[] = $key."=".urlencode($val);
            }
            $strPOST =  join("&", $aPOST);
        }
        curl_setopt($oCurl, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($oCurl, CURLOPT_URL, $url);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1 );
        curl_setopt($oCurl, CURLOPT_POST,true);
        curl_setopt($oCurl, CURLOPT_POSTFIELDS,$strPOST);
        curl_setopt($oCurl, CURLOPT_HTTPHEADER, $headers);
        $sContent = curl_exec($oCurl);
        $aStatus = curl_getinfo($oCurl);
    
        $errno = curl_errno($oCurl);
        if($errno) {
            Log::error("curl异常:", [
                'url' => $url,
                'params' => $param,
                'errno' => $errno,
            ]);
        }
    
        curl_close($oCurl);
        if(intval($aStatus["http_code"])==200){
            return $sContent;
        }else{
            Log::error("curl 不为200:", [
                'url' => $url,
                'params' => $param,
                'errno' => $errno,
                'rtn' => $sContent,
            ]);
            return false;
        }
    }
    
    /**
     * 随机字符串,不长于32位。
     * @param int $length
     * @return string
     */
    private function getNonceStr(int $length = 32) :string
    {
        $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
        $str = '';
        for ($i = 0; $i<$length; $i++) {
            $str .=substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }
    

    getOpenidFromMp() 方法

    private function getOpenidFromMp($code, $get_config)
        {
            $url = $this->__CreateOauthUrlForOpenid($code, $get_config);
    
            //初始化curl
            $ch = curl_init();
            $curlVersion = curl_version();
            $ua = "WXPaySDK/3.0.9 (".PHP_OS.") PHP/".PHP_VERSION." CURL/".$curlVersion['version']." "
                .'1481215812';
    
            //设置超时
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,FALSE);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,FALSE);
            curl_setopt($ch, CURLOPT_USERAGENT, $ua);
            curl_setopt($ch, CURLOPT_HEADER, FALSE);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    
            $proxyHost = "0.0.0.0";
            $proxyPort = 0;
            if($proxyHost != "0.0.0.0" && $proxyPort != 0){
                curl_setopt($ch,CURLOPT_PROXY, $proxyHost);
                curl_setopt($ch,CURLOPT_PROXYPORT, $proxyPort);
            }
            //运行curl,结果以jason形式返回
            $res = curl_exec($ch);
    
            if (curl_errno($ch)) {
                Log::error("curl异常 MiniAppAutoReply GetOpenidFromMp:", [
                    'url' => $url,
                    'params' => $code,
                    'errno' => curl_errno($ch),
                ]);
            }
    
            curl_close($ch);
            //取出openid
            $data = json_decode($res,true);
    
            if (isset($data['openid'])) {
                $openid = $data['openid'];
            } else {
                Log::info(__CLASS__.';'.__FUNCTION__.";请求获取openid 出错,结果为:",[ $data ]);
                $openid = '';
            }
            return $openid;
        }
    

    构造获取open和access_toke的url地址 方法__CreateOauthUrlForOpenid()

        /**
         * 构造获取open和access_toke的url地址
         * @param $code
         * @param array $get_conf
         * @return string 请求的url
         */
        private function __CreateOauthUrlForOpenid($code, $get_conf)
        {
            $urlObj["appid"] = $get_conf['zbAppId'];//应用ID
            $urlObj["secret"] = $get_conf['zbAppSecret'];//商户密钥
            $urlObj["js_code"] = $code;
            $urlObj["grant_type"] = "authorization_code";
            $bizString = $this->ToUrlParams($urlObj);
            //return "https://api.weixin.qq.com/sns/oauth2/access_token?".$bizString;
            return "https://api.weixin.qq.com/sns/jscode2session?".$bizString;
        }
    

    拼接签名字符串方法 ToUrlParams()

        /**
         * 拼接签名字符串
         * @param array $urlObj
         * @return string 返回已经拼接好的字符串
         */
        private function ToUrlParams($urlObj)
        {
            $buff = "";
            foreach ($urlObj as $k => $v)
            {
                if($k != "sign"){
                    $buff .= $k . "=" . $v . "&";
                }
            }
    
            $buff = trim($buff, "&");
            return $buff;
        }
    

    调用wx.requestPayment(OBJECT)发起微信支付成功后,支付回调处理

    public function payCallBack()
    {
        try {
            $params = \request()->all();//请求参数
            /**
             * 微信小程序回调文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
             * 微信小程序APIv3支付回调解密  https://blog.csdn.net/qq_16746785/article/details/120260761
             *
             * 支付成功结果通知
             * {
                "id": "EV-2018022511223320873",
                "create_time": "2015-05-20T13:29:35+08:00",
                "resource_type": "encrypt-resource",
                "event_type": "TRANSACTION.SUCCESS",
                "summary": "支付成功",
                "resource": {
                    "original_type": "transaction",
                    "algorithm": "AEAD_AES_256_GCM",
                    "ciphertext": "",
                    "associated_data": "",
                    "nonce": ""
                }
            }
             */
            if (isset($params['summary']) && $params['summary'] == '支付成功') {
                $nonce_str = $params['resource']['nonce'];
                $associated_data = $params['resource']['associated_data'];
                $cipher_text = base64_decode($params['resource']['ciphertext']);
                if (strlen($cipher_text) <= 12) {
                    Log::warning(__CLASS__ .';' . __FUNCTION__ . ";微信小程序APIv3支付回调解密失败 cipher_text <= 12");
                }
    
                $get_config = $this->config();
                $key = $get_config['zbApiV3Key'];//商户平台设置的api v3 密钥
                // sodium_crypto_aead_aes256gcm_decrypt ( string $ciphertext , string $ad , string $nonce , string $key ) 这个函数。
                //这个函数调用的时候会报错,那是因为使用这个函数需要开启 libsodium 扩展才能使用。需要先自行安装下这个扩展
                $result = json_decode( sodium_crypto_aead_aes256gcm_decrypt($cipher_text, $associated_data, $nonce_str, $key), true );
    //                $dd = [
    //                    'appid' => $result['appid'],//应用ID
    //                    'mchid' => $result['mchid'],//商户号
    //                    'out_trade_no' => $result['out_trade_no'],//商户订单号
    //                    'transaction_id' => $result['transaction_id'],//微信支付订单号
    //                    /**
    //                     * 交易类型,枚举值:
    //                        JSAPI:公众号支付
    //                        NATIVE:扫码支付
    //                        APP:APP支付
    //                        MICROPAY:付款码支付
    //                        MWEB:H5支付
    //                        FACEPAY:刷脸支付
    //                        示例值:MICROPAY
    //                     */
    //                    'trade_type' => $result['trade_type'],
    //                    'openid' => $result['payer']['openid'],//payer 支付者信息/openid 用户标识
    //                    'payer_total' => $result['amount']['payer_total'],//amount 订单金额信息/payer_total 用户支付金额,单位为分
    //                    /**
    //                     * 交易状态,枚举值:
    //                        SUCCESS:支付成功
    //                        REFUND:转入退款
    //                        NOTPAY:未支付
    //                        CLOSED:已关闭
    //                        REVOKED:已撤销(付款码支付)
    //                        USERPAYING:用户支付中(付款码支付)
    //                        PAYERROR:支付失败(其他原因,如银行返回失败)
    //                     */
    //                    'trade_state' => $result['trade_state'],//交易状态
    //                ];
                //Log::debug(__CLASS__ .';' . __FUNCTION__ . ";微信小程序APIv3支付回调解密 resource对象 结果", [$result]);
            }
    
            if($result['trade_state'] == 'SUCCESS') {
                // TODO 操作发货,更新订单信息等等。。
            } else {
                Log::warning(__CLASS__ .';' . __FUNCTION__ . ";支付通知API 回调结果不是成功的 trade_state:".$result['trade_state']);
            }
    
        } catch (Exception $e) {
            Log::warning(__CLASS__ .';' . __FUNCTION__ . ";发货异常:" . $e->getMessage() . ";所在行数:" . $e->getLine());
        }
    
        return ['code' => 'SUCCESS', 'msg' => '成功'];
    }
    

    相关文章

      网友评论

          本文标题:微信小程序JSAPI下单 APIv3 PHP

          本文链接:https://www.haomeiwen.com/subject/ruemrrtx.html